Compare commits
	
		
			64 Commits
		
	
	
		
			c6a9fc28a3
			...
			efaf906f0a
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						efaf906f0a | |
| 
							
							
								
								 | 
						3e865e5575 | |
| 
							
							
								 | 
						ee281986b8 | |
| 
							
							
								
								 | 
						6e1e452bf1 | |
| 
							
							
								
								 | 
						6528b7bc59 | |
| 
							
							
								 | 
						fd1be27414 | |
| 
							
							
								 | 
						d064b242e6 | |
| 
							
							
								
								 | 
						8f6977be72 | |
| 
							
							
								
								 | 
						af67644416 | |
| 
							
							
								 | 
						705e8f9216 | |
| 
							
							
								
								 | 
						db4feedde3 | |
| 
							
							
								 | 
						27b59ae939 | |
| 
							
							
								 | 
						034b9eee2c | |
| 
							
							
								
								 | 
						543b1af58c | |
| 
							
							
								 | 
						d09fa6d432 | |
| 
							
							
								 | 
						40f80579dc | |
| 
							
							
								
								 | 
						b1e7aaa1ea | |
| 
							
							
								
								 | 
						a883662f0b | |
| 
							
							
								
								 | 
						a11f67f252 | |
| 
							
							
								 | 
						fb6c58f1c0 | |
| 
							
							
								 | 
						c2c9322e7d | |
| 
							
							
								 | 
						3263bce784 | |
| 
							
							
								 | 
						500d6ec34d | |
| 
							
							
								 | 
						1b35f8481b | |
| 
							
							
								
								 | 
						172af363de | |
| 
							
							
								 | 
						cd9c01cdee | |
| 
							
							
								 | 
						2c8bb92775 | |
| 
							
							
								 | 
						5b65415d76 | |
| 
							
							
								 | 
						655d9cd4c6 | |
| 
							
							
								 | 
						e05e92b96b | |
| 
							
							
								
								 | 
						1328320713 | |
| 
							
							
								 | 
						7217a83e33 | |
| 
							
							
								 | 
						8474bd97a1 | |
| 
							
							
								
								 | 
						f8c2af2ec8 | |
| 
							
							
								 | 
						5f8b03f558 | |
| 
							
							
								 | 
						fa47d32b07 | |
| 
							
							
								
								 | 
						160596a7a3 | |
| 
							
							
								 | 
						177296a324 | |
| 
							
							
								 | 
						d5936087ff | |
| 
							
							
								 | 
						951c3be613 | |
| 
							
							
								 | 
						b5a7740d9f | |
| 
							
							
								 | 
						2011797dd6 | |
| 
							
							
								 | 
						7dc31bf75b | |
| 
							
							
								 | 
						ca766fc293 | |
| 
							
							
								 | 
						32b197969c | |
| 
							
							
								 | 
						462d896e57 | |
| 
							
							
								 | 
						44fa6b55f7 | |
| 
							
							
								 | 
						ddcb78ef98 | |
| 
							
							
								 | 
						d61fd879b7 | |
| 
							
							
								 | 
						d3c1d9bf60 | |
| 
							
							
								 | 
						65c4dbcf88 | |
| 
							
							
								 | 
						af5721682b | |
| 
							
							
								 | 
						308f5a3a2e | |
| 
							
							
								 | 
						05cb3f722a | |
| 
							
							
								 | 
						1f9ab744ae | |
| 
							
							
								 | 
						f807a635bb | |
| 
							
							
								 | 
						d2c0e0b95d | |
| 
							
							
								 | 
						4da48df8d7 | |
| 
							
							
								 | 
						5cd7071459 | |
| 
							
							
								 | 
						6e2a8c2578 | |
| 
							
							
								
								 | 
						db5c9b09a8 | |
| 
							
							
								 | 
						650cfe1b13 | |
| 
							
							
								 | 
						8ce0d957b1 | |
| 
							
							
								 | 
						86f2bb12fc | 
| 
						 | 
				
			
			@ -14,7 +14,7 @@ class CategoryController extends Controller
 | 
			
		|||
     * @param Request $request The HTTP request object.
 | 
			
		||||
     * @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
 | 
			
		||||
     */
 | 
			
		||||
    public function getListMaster(Request $request)
 | 
			
		||||
    public static function getListMaster(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $data = Category::where('c_type', '=', $request->type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
 | 
			
		||||
        return AbstractController::ResultSuccess($data);
 | 
			
		||||
| 
						 | 
				
			
			@ -24,4 +24,9 @@ class CategoryController extends Controller
 | 
			
		|||
        $data = Category::where('c_type', '=', $type)->where('c_code', '=', $code)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->first();
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
    public static function getListMasterByType($type)
 | 
			
		||||
    {
 | 
			
		||||
        $data = Category::where('c_type', '=', $type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,31 +35,34 @@ class LeaveManagementController extends Controller
 | 
			
		|||
        })
 | 
			
		||||
            ->leftJoin("categories as reason", function ($join) {
 | 
			
		||||
                $join->on('n_reason', '=', 'reason.c_code');
 | 
			
		||||
                $join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
 | 
			
		||||
                $join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
 | 
			
		||||
            })
 | 
			
		||||
            ->select(
 | 
			
		||||
                DB::raw('notes.id as id'),
 | 
			
		||||
                DB::raw('notes.n_user_id as n_user_id'),
 | 
			
		||||
                DB::raw('notes.n_time_type as time_type'),
 | 
			
		||||
                DB::raw('notes.n_year as year'),
 | 
			
		||||
                DB::raw('notes.n_month as month'),
 | 
			
		||||
                DB::raw('categories.c_value as leave_days'),
 | 
			
		||||
                DB::raw('notes.n_day as day'),
 | 
			
		||||
                DB::raw('notes.n_reason as reason_code'),
 | 
			
		||||
                'reason.c_name as reason_name',
 | 
			
		||||
                'categories.c_name as time_type_name',
 | 
			
		||||
                // DB::raw('SUM(categories.c_value) as leave_days')
 | 
			
		||||
            )
 | 
			
		||||
            // ->where('notes.n_user_id', "1")
 | 
			
		||||
            ->where('notes.n_year', $year)
 | 
			
		||||
            ->where('notes.n_reason', 'ONLEAVE')
 | 
			
		||||
            ->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY', 'TEMPORARY_ONLEAVE'])
 | 
			
		||||
            // ->groupBy("notes.n_user_id")
 | 
			
		||||
            ->orderBy('notes.n_month')
 | 
			
		||||
            ->orderBy('notes.n_day')
 | 
			
		||||
            ->get()
 | 
			
		||||
            ->map(function ($item) {
 | 
			
		||||
                return [
 | 
			
		||||
                    "id" => $item->id,
 | 
			
		||||
                    "day" => $item->day,
 | 
			
		||||
                    "n_user_id" => $item->n_user_id,
 | 
			
		||||
                    // "time_type" => $item->time_type,
 | 
			
		||||
                    "reason_code" => $item->reason_code,
 | 
			
		||||
                    "reason_name" => $item->reason_name,
 | 
			
		||||
                    "time_type_name" => $item->time_type_name,
 | 
			
		||||
                    "month" => $item->month,
 | 
			
		||||
| 
						 | 
				
			
			@ -105,9 +108,10 @@ class LeaveManagementController extends Controller
 | 
			
		|||
                    'leaveDay' => [
 | 
			
		||||
                        'id' => $item->id,
 | 
			
		||||
                        'ld_user_id' => $item->ld_user_id,
 | 
			
		||||
                        'ld_day' => $item->ld_day,
 | 
			
		||||
                        'ld_day_total' => $item->ld_day_total,
 | 
			
		||||
                        'ld_year' => $item->ld_year,
 | 
			
		||||
                        'ld_date_additional' => $item->ld_date_additional,
 | 
			
		||||
                        'ld_additional_day' => $item->ld_additional_day,
 | 
			
		||||
                        'ld_special_leave_day' => $item->ld_special_leave_day,
 | 
			
		||||
                        'ld_note' => $item->ld_note,
 | 
			
		||||
                        'created_at' => $item->created_at,
 | 
			
		||||
                        'updated_at' => $item->updated_at,
 | 
			
		||||
| 
						 | 
				
			
			@ -133,8 +137,9 @@ class LeaveManagementController extends Controller
 | 
			
		|||
        $validatedData = $request->all();
 | 
			
		||||
        $leaveDays = LeaveDays::find($validatedData['id']);
 | 
			
		||||
 | 
			
		||||
        $leaveDays->ld_day = $validatedData['totalLeave'];
 | 
			
		||||
        $leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
 | 
			
		||||
        $leaveDays->ld_day_total = $validatedData['totalLeave'];
 | 
			
		||||
        $leaveDays->ld_additional_day = $validatedData['dayAdditional'];
 | 
			
		||||
        $leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
 | 
			
		||||
        $leaveDays->ld_note = $validatedData['note'];
 | 
			
		||||
 | 
			
		||||
        $leaveDays->save();
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +147,28 @@ class LeaveManagementController extends Controller
 | 
			
		|||
        return response()->json(['status' => true, 'message' => 'Updated successfully']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function updateNoteStatus(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $rules = [
 | 
			
		||||
            'id' => 'required',
 | 
			
		||||
            'n_reason' => 'required|in:ONLEAVE,LEAVE_WITHOUT_PAY'
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // Validate the request
 | 
			
		||||
        $request->validate($rules);
 | 
			
		||||
        $id = $request->input('id');
 | 
			
		||||
        $reason = $request->input('n_reason');
 | 
			
		||||
 | 
			
		||||
        $note = Notes::find($id);
 | 
			
		||||
        if (!$note) {
 | 
			
		||||
            return response()->json(['message' => 'Note not found', 'status' => false]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $note->n_reason = $reason;
 | 
			
		||||
        $note->save();
 | 
			
		||||
        return response()->json(data: ['message' => 'Update success', 'status' => true]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function export(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $year = $request->query('year', now()->year);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -11,12 +11,14 @@ use App\Traits\HasOrderByRequest;
 | 
			
		|||
use App\Traits\HasSearchRequest;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Facades\Mail;
 | 
			
		||||
use App\Mail\TicketMail;
 | 
			
		||||
use Modules\Admin\app\Models\Admin;
 | 
			
		||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
 | 
			
		||||
use Modules\Admin\app\Models\Tracking;
 | 
			
		||||
use Maatwebsite\Excel\Facades\Excel;
 | 
			
		||||
use App\Exports\TimekeepingExport;
 | 
			
		||||
use Modules\Admin\app\Models\Ticket;
 | 
			
		||||
 | 
			
		||||
class TimekeepingController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -152,8 +154,6 @@ class TimekeepingController extends Controller
 | 
			
		|||
        return response()->json(['status' => true, 'message' => 'Add successfully']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function updateCacheMonth(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $month = $request->month;
 | 
			
		||||
| 
						 | 
				
			
			@ -172,20 +172,64 @@ class TimekeepingController extends Controller
 | 
			
		|||
 | 
			
		||||
        // Validate the request
 | 
			
		||||
        $request->validate($rules);
 | 
			
		||||
 | 
			
		||||
        $id = $request->input('id');
 | 
			
		||||
 | 
			
		||||
        $month = $request->month;
 | 
			
		||||
        $year = $request->year;
 | 
			
		||||
 | 
			
		||||
        $note = Notes::find($id);
 | 
			
		||||
        if ($note) {
 | 
			
		||||
            $note->delete();
 | 
			
		||||
            $this->createOrUpdateRecordForCurrentMonth($month, $year);
 | 
			
		||||
        if (!$note) {
 | 
			
		||||
            return response()->json(['message' => 'Note not found', 'status' => false]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($note->ticket_id != null) {
 | 
			
		||||
            $ticket = Ticket::find($note->ticket_id);
 | 
			
		||||
            if (!$ticket) {
 | 
			
		||||
                return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $admin = auth('admins')->user();
 | 
			
		||||
 | 
			
		||||
            // Handle send mail
 | 
			
		||||
            $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->start_period);
 | 
			
		||||
            $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->end_period);
 | 
			
		||||
            $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $ticket->type);
 | 
			
		||||
            $formattedStartDate = Carbon::createFromFormat('Y-m-d', $ticket->start_date)->format('d/m/Y');
 | 
			
		||||
            $formattedEndDate = Carbon::createFromFormat('Y-m-d', $ticket->end_date)->format('d/m/Y');
 | 
			
		||||
 | 
			
		||||
            $user = Admin::find($ticket->user_id);
 | 
			
		||||
 | 
			
		||||
            $data = array(
 | 
			
		||||
                "email_template" => "email.notification_tickets_user",
 | 
			
		||||
                "user_name" => $user->name,
 | 
			
		||||
                "email" => $user->email,
 | 
			
		||||
                "name" => $admin->name, //name admin duyệt
 | 
			
		||||
                "date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
 | 
			
		||||
                "type" => $dataMasterType->c_name,
 | 
			
		||||
                "note" => $ticket->reason,
 | 
			
		||||
                "admin_note" => $ticket->admin_note,
 | 
			
		||||
                "link" => "/tickets", //link đến page admin
 | 
			
		||||
                "status" => "refused",
 | 
			
		||||
                "subject" => "[Ticket response] Ticket From " . $admin->name
 | 
			
		||||
            );
 | 
			
		||||
            Mail::to($user->email)->send(new TicketMail($data));
 | 
			
		||||
 | 
			
		||||
            // Update
 | 
			
		||||
            $ticket->updated_by = $admin->name;
 | 
			
		||||
            $ticket->status = "REFUSED";
 | 
			
		||||
            $ticket->save();
 | 
			
		||||
            Notes::where('ticket_id', $ticket->id)->delete();
 | 
			
		||||
 | 
			
		||||
            // Clear Timekeeping cache
 | 
			
		||||
            $this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
 | 
			
		||||
            $this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
 | 
			
		||||
 | 
			
		||||
            return response()->json(['message' => 'Delete success', 'status' => true]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response()->json(['message' => 'Delete fail', 'status' => false]);
 | 
			
		||||
        $note->delete();
 | 
			
		||||
        $this->createOrUpdateRecordForCurrentMonth($month, $year);
 | 
			
		||||
 | 
			
		||||
        return response()->json(['message' => 'Delete success', 'status' => true]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function export(Request $request)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ Route::middleware('api')
 | 
			
		|||
            Route::post('login', [AdminController::class, 'login']);
 | 
			
		||||
            Route::post('reset-password', [AdminController::class, 'resetPassword']);
 | 
			
		||||
            Route::get('forgot-password', [AdminController::class, 'forgotPassword']);
 | 
			
		||||
            Route::get('/email-handle-ticket', [TicketController::class, 'handleTicketEmail'])->name('email.ticket.handle');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // NOTE after login
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +157,7 @@ Route::middleware('api')
 | 
			
		|||
                Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::post('/updateNoteStatus', [LeaveManagementController::class, 'updateNoteStatus'])->middleware('check.permission:admin.hr');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
| 
						 | 
				
			
			@ -163,6 +165,7 @@ Route::middleware('api')
 | 
			
		|||
            ], function () {
 | 
			
		||||
                Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +177,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([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,18 +3,23 @@
 | 
			
		|||
namespace Modules\Auth\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Models\LeaveDays;
 | 
			
		||||
use App\Traits\IsAPI;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Support\Facades\Http;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Modules\Auth\app\Models\User;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Modules\Admin\app\Models\Category;
 | 
			
		||||
use SimpleSoftwareIO\QrCode\Facades\QrCode;
 | 
			
		||||
use App\Traits\AnalyzeData;
 | 
			
		||||
 | 
			
		||||
class UserController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    use IsAPI;
 | 
			
		||||
    use AnalyzeData;
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->middleware('jwt.auth');
 | 
			
		||||
| 
						 | 
				
			
			@ -34,9 +39,37 @@ class UserController extends Controller
 | 
			
		|||
        ]);
 | 
			
		||||
 | 
			
		||||
        if ($request->has('id')) {
 | 
			
		||||
            $payload = $request->only(['name', 'email', 'permission']);
 | 
			
		||||
            $payload = $request->only(['name', 'email', 'permission', 'is_permanent']);
 | 
			
		||||
            $user = User::find($request->id);
 | 
			
		||||
 | 
			
		||||
            // Không cho chuyển từ chính thức thành lại thử việc
 | 
			
		||||
            if (!$request->is_permanent && $user->is_permanent) {
 | 
			
		||||
                return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Thêm ngày phép khi thành nhân viên chính thức
 | 
			
		||||
            if ($request->is_permanent && !$user->is_permanent) {
 | 
			
		||||
                $userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
 | 
			
		||||
                    ->where('ld_year', Carbon::now()->year)
 | 
			
		||||
                    ->first();
 | 
			
		||||
 | 
			
		||||
                if ($userLeaveDay) {
 | 
			
		||||
                    $permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
 | 
			
		||||
                    $permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
 | 
			
		||||
                    $userLeaveDay->ld_day_total = $permanentDefault;
 | 
			
		||||
 | 
			
		||||
                    $newNote = "Cộng ngày phép cho nhân viên chính thức"; // Thêm ghi chú
 | 
			
		||||
                    if (!empty($userLeaveDay->ld_note)) {
 | 
			
		||||
                        $userLeaveDay->ld_note = $userLeaveDay->ld_note . "\n" . $newNote;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        $userLeaveDay->ld_note = $newNote;
 | 
			
		||||
                    }
 | 
			
		||||
                    $userLeaveDay->save();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $payload['permanent_date'] = Carbon::now()->toDateString();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $user->update($payload);
 | 
			
		||||
            return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,9 +77,22 @@ class UserController extends Controller
 | 
			
		|||
                'name' => $request->name,
 | 
			
		||||
                'email' => $request->email,
 | 
			
		||||
                'password' => bcrypt('Work@1234'),
 | 
			
		||||
                'permission' => $request->permission
 | 
			
		||||
                'permission' => $request->permission,
 | 
			
		||||
                'is_permanent' => false
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Khởi tạo LeaveDays cho nhân viên mới
 | 
			
		||||
            LeaveDays::insert([
 | 
			
		||||
                'ld_user_id' => $user->id,
 | 
			
		||||
                'ld_day_total' => 0,
 | 
			
		||||
                'ld_year' => Carbon::now()->year,
 | 
			
		||||
                'ld_additional_day' => 0,
 | 
			
		||||
                'ld_note' => '',
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ]);
 | 
			
		||||
            $this->createOrUpdateRecordForCurrentMonth(Carbon::now()->month, Carbon::now()->year);
 | 
			
		||||
 | 
			
		||||
            $user_res = [
 | 
			
		||||
                'name' => $user->name,
 | 
			
		||||
                'email' => $user->email,
 | 
			
		||||
| 
						 | 
				
			
			@ -98,8 +144,6 @@ class UserController extends Controller
 | 
			
		|||
                return response()->json(['data' => ['user' => $user_res, 'gitea' => "dev", 'zulip' => "dev"], 'status' => true, 'message' => 'Create successful']);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response()->json(['status' => false, 'message' => 'Process fail']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function delete(Request $request)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,9 @@ class User extends Authenticatable implements JWTSubject
 | 
			
		|||
        'name',
 | 
			
		||||
        'email',
 | 
			
		||||
        'password',
 | 
			
		||||
        'permission'
 | 
			
		||||
        'permission',
 | 
			
		||||
        'is_permanent',
 | 
			
		||||
        'permanent_date'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Console\Commands;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use App\Jobs\AddMonthlyLeaveDays;
 | 
			
		||||
 | 
			
		||||
class AddMonthlyLeaveDaysCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected $signature = 'add:monthly-leavedays {month?} {year?}';
 | 
			
		||||
    protected $description = 'Cộng 1 ngày phép hàng tháng cho tất cả người dùng';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        $month = $this->argument('month');
 | 
			
		||||
        $year = $this->argument('year');
 | 
			
		||||
        AddMonthlyLeaveDays::dispatch($month, $year);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ use App\Jobs\InitializeLeaveDays;
 | 
			
		|||
class InitializeLeaveDaysCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected $signature = 'initialize:leavedays {year?}';
 | 
			
		||||
    protected $description = 'Initialize leave days for users';
 | 
			
		||||
    protected $description = 'Cấp phép năm cho tất cả người dùng';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ class InitializeLeaveDaysCommand extends Command
 | 
			
		|||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        $year = $this->argument('year');
 | 
			
		||||
        InitializeLeaveDays::dispatch($year);
 | 
			
		||||
        // Không sử dụng nữa, theo rule mới
 | 
			
		||||
        // InitializeLeaveDays::dispatch($year);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Console\Commands;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use App\Jobs\UpdateTemporaryLeaveDays;
 | 
			
		||||
 | 
			
		||||
class UpdateTemporaryLeaveDaysCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    protected $signature = 'update:temporary-leavedays {month?} {year?}';
 | 
			
		||||
    protected $description = 'Tính lại ngày phép cho các note tạm.';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        $month = $this->argument('month');
 | 
			
		||||
        $year = $this->argument('year');
 | 
			
		||||
        UpdateTemporaryLeaveDays::dispatch($month, $year);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
namespace App\Console;
 | 
			
		||||
 | 
			
		||||
use App\Jobs\DeductLeaveDays;
 | 
			
		||||
use App\Jobs\AddMonthlyLeaveDays;
 | 
			
		||||
use Illuminate\Console\Scheduling\Schedule;
 | 
			
		||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +33,10 @@ class Kernel extends ConsoleKernel
 | 
			
		|||
 | 
			
		||||
        // Chạy buổi chiều lúc 17:30
 | 
			
		||||
        $schedule->command('attendance:check C')->dailyAt('17:30');
 | 
			
		||||
 | 
			
		||||
        // Chạy vào ngày đầu tiên của mỗi tháng
 | 
			
		||||
        $schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
 | 
			
		||||
        $schedule->command('update:temporary-leavedays')->monthlyOn(1, '00:05');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, With
 | 
			
		|||
        $stt = 0;
 | 
			
		||||
        foreach ($this->data as $index => $user) {
 | 
			
		||||
            $totalDayOff = 0;
 | 
			
		||||
            $totalDayLeave = $user['leaveDay']['ld_day'] + $user['leaveDay']['ld_date_additional'];
 | 
			
		||||
            $totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day'];
 | 
			
		||||
 | 
			
		||||
            // Tính tổng ngày nghỉ theo tháng
 | 
			
		||||
            $monthlyLeaves = array_fill(1, 12, 0);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Jobs;
 | 
			
		||||
 | 
			
		||||
use App\Models\LeaveDays;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
			
		||||
use Illuminate\Queue\InteractsWithQueue;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
use Modules\Admin\app\Models\Category;
 | 
			
		||||
 | 
			
		||||
class AddMonthlyLeaveDays implements ShouldQueue
 | 
			
		||||
{
 | 
			
		||||
  use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 | 
			
		||||
 | 
			
		||||
  protected $month;
 | 
			
		||||
  protected $year;
 | 
			
		||||
 | 
			
		||||
  private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng
 | 
			
		||||
 | 
			
		||||
  public function __construct($month = null, $year = null)
 | 
			
		||||
  {
 | 
			
		||||
    $this->month = $month ?? Carbon::now()->month;
 | 
			
		||||
    $this->year = $year ?? Carbon::now()->year;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public function handle(): void
 | 
			
		||||
  {
 | 
			
		||||
    $users = User::get();
 | 
			
		||||
 | 
			
		||||
    foreach ($users as $user) {
 | 
			
		||||
      // Nếu là nhân viên chưa chính thức, ko cộng phép
 | 
			
		||||
      if (!$user->is_permanent) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $leaveDay = LeaveDays::where('ld_user_id', $user->id)
 | 
			
		||||
        ->where('ld_year', $this->year)
 | 
			
		||||
        ->first();
 | 
			
		||||
 | 
			
		||||
      if (!$leaveDay) {
 | 
			
		||||
        // Nếu chưa có dữ liệu năm hiện tại, tạo mới
 | 
			
		||||
        // Số ngày phép bằng với tháng hiện tại
 | 
			
		||||
        $leaveDay = new LeaveDays([
 | 
			
		||||
          'ld_user_id' => $user->id,
 | 
			
		||||
          'ld_day_total' => $this->month, // Số ngày phép bằng tháng hiện tại
 | 
			
		||||
          'ld_year' => $this->year,
 | 
			
		||||
          'ld_additional_day' => 0,
 | 
			
		||||
          'ld_note' => 'Khởi tạo ngày phép đến tháng ' . $this->month,
 | 
			
		||||
          'ld_special_leave_day' => 0,
 | 
			
		||||
        ]);
 | 
			
		||||
        $leaveDay->save();
 | 
			
		||||
      } else {
 | 
			
		||||
        // Check có phải là nhân viên chính thức trong năm nay (Nhân viên mới)
 | 
			
		||||
        if ($user->permanent_date && $user->permanent_date !== '0000-00-00') {
 | 
			
		||||
          $permenantYear  = Carbon::parse($user->permanent_date)->year;
 | 
			
		||||
 | 
			
		||||
          if ($permenantYear === $this->year) {
 | 
			
		||||
            $permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
 | 
			
		||||
            $permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
 | 
			
		||||
 | 
			
		||||
            $permanentMonth = Carbon::parse($user->permanent_date)->month;
 | 
			
		||||
            if ($this->month > $leaveDay->ld_day_total - ($permanentDefault - $permanentMonth)) {
 | 
			
		||||
              $leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
 | 
			
		||||
              $leaveDay->save();
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại (Nhân viên cũ)
 | 
			
		||||
        if ($leaveDay->ld_day_total < $this->month) {
 | 
			
		||||
          // Cộng mỗi tháng 1 ngày phép cho nhân viên
 | 
			
		||||
          $leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
 | 
			
		||||
          $leaveDay->save();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		|||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
			
		||||
use Illuminate\Queue\InteractsWithQueue;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
 | 
			
		||||
class DeductLeaveDays implements ShouldQueue
 | 
			
		||||
| 
						 | 
				
			
			@ -36,44 +35,34 @@ class DeductLeaveDays implements ShouldQueue
 | 
			
		|||
        foreach ($users as $user) {
 | 
			
		||||
            $existingData = LeaveDays::where('ld_user_id', $user->id)
 | 
			
		||||
                ->where('ld_year', $this->year)
 | 
			
		||||
                ->where('ld_date_additional', ">", 0)
 | 
			
		||||
                ->where('ld_additional_day', ">", 0)
 | 
			
		||||
                ->first();
 | 
			
		||||
            if (!$existingData) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
 | 
			
		||||
            // Lấy tổng ngày nghỉ phép 3 tháng đầu trong năm
 | 
			
		||||
            $usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
 | 
			
		||||
                $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
			
		||||
                    ->where('categories.c_type', 'TIME_TYPE');
 | 
			
		||||
            })
 | 
			
		||||
                ->select(
 | 
			
		||||
                    DB::raw('notes.n_user_id as n_user_id'),
 | 
			
		||||
                    DB::raw('notes.n_year as year'),
 | 
			
		||||
                    DB::raw('SUM(categories.c_value) as leave_days')
 | 
			
		||||
                )
 | 
			
		||||
                ->where('notes.n_year', $this->year)
 | 
			
		||||
                ->where('notes.n_user_id', $user->id)
 | 
			
		||||
                ->where('notes.n_reason', 'ONLEAVE')
 | 
			
		||||
                ->groupBy(DB::raw('notes.n_year'))
 | 
			
		||||
                ->first();
 | 
			
		||||
                ->where('n_user_id', $user->id)
 | 
			
		||||
                ->where('n_year', $this->year)
 | 
			
		||||
                ->where('n_month', "<=", 3)
 | 
			
		||||
                ->where('n_reason', 'ONLEAVE')
 | 
			
		||||
                ->sum('categories.c_value');
 | 
			
		||||
 | 
			
		||||
            if ($totalLeaveDaysByMonth) {
 | 
			
		||||
                //Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng)
 | 
			
		||||
                if ($existingData->ld_date_additional > $totalLeaveDaysByMonth->leave_days) {
 | 
			
		||||
                    LeaveDays::where('ld_year', $this->year)
 | 
			
		||||
                        ->where('ld_user_id', $user->id)
 | 
			
		||||
                        ->update([
 | 
			
		||||
                            'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
 | 
			
		||||
                        ]);
 | 
			
		||||
            if ($usedOnleaveDaysTotal) {
 | 
			
		||||
                if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
 | 
			
		||||
                    $ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
 | 
			
		||||
                    $existingData->ld_note = $existingData->ld_note . "\n" . $ld_note;
 | 
			
		||||
                    $existingData->ld_additional_day = $usedOnleaveDaysTotal;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                //Nếu không sử dụng ngày nghỉ còn lại ở năm rồi thì xóa => theo luật ld
 | 
			
		||||
                LeaveDays::where('ld_year', $this->year)
 | 
			
		||||
                    ->where('ld_user_id', $user->id)
 | 
			
		||||
                    ->update([
 | 
			
		||||
                        'ld_date_additional' => "0",
 | 
			
		||||
                    ]);
 | 
			
		||||
            }
 | 
			
		||||
                $existingData->ld_additional_day = 0;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $existingData->save();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,8 @@ class InitializeLeaveDays implements ShouldQueue
 | 
			
		|||
    public function handle(): void
 | 
			
		||||
    {
 | 
			
		||||
        $users = User::get();
 | 
			
		||||
        $ld_day = 12;
 | 
			
		||||
        $ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
 | 
			
		||||
 | 
			
		||||
        foreach ($users as $user) {
 | 
			
		||||
            // Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
 | 
			
		||||
            $existingData = LeaveDays::where('ld_user_id', $user->id)
 | 
			
		||||
| 
						 | 
				
			
			@ -51,11 +52,11 @@ class InitializeLeaveDays implements ShouldQueue
 | 
			
		|||
                ->where('ld_year', $this->year - 1)
 | 
			
		||||
                ->first();
 | 
			
		||||
 | 
			
		||||
            $ld_date_additional = 0;
 | 
			
		||||
            $ld_additional_day = 0;
 | 
			
		||||
            $ld_note = '';
 | 
			
		||||
 | 
			
		||||
            if ($previousYearData) {
 | 
			
		||||
                $ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional;
 | 
			
		||||
                $ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day;
 | 
			
		||||
                $totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
 | 
			
		||||
                    $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
			
		||||
                        ->where('categories.c_type', 'TIME_TYPE');
 | 
			
		||||
| 
						 | 
				
			
			@ -71,20 +72,23 @@ class InitializeLeaveDays implements ShouldQueue
 | 
			
		|||
                    ->groupBy(DB::raw('notes.n_year'))
 | 
			
		||||
                    ->first();
 | 
			
		||||
                if ($totalLeaveDaysByMonth) {
 | 
			
		||||
                    $ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
 | 
			
		||||
                    if ($ld_date_additional < 0) {
 | 
			
		||||
                        $ld_date_additional = 0;
 | 
			
		||||
                    $ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
 | 
			
		||||
                    if ($ld_additional_day < 0) {
 | 
			
		||||
                        $ld_additional_day = 0;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $ld_note = 'Cộng dồn ngày phép năm cũ';
 | 
			
		||||
 | 
			
		||||
                if ($ld_additional_day > 0) {
 | 
			
		||||
                    $ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Tạo dữ liệu cho năm hiện tại
 | 
			
		||||
            LeaveDays::insert([
 | 
			
		||||
                'ld_user_id' => $user->id,
 | 
			
		||||
                'ld_day' => $ld_day,
 | 
			
		||||
                'ld_day_total' => $user->is_permanent ? $ld_day_total : 0, // Nếu là nhân viên mới, ko cấp phép
 | 
			
		||||
                'ld_year' => $this->year,
 | 
			
		||||
                'ld_date_additional' => $ld_date_additional,
 | 
			
		||||
                'ld_additional_day' => $ld_additional_day,
 | 
			
		||||
                'ld_note' => $ld_note,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,220 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Jobs;
 | 
			
		||||
 | 
			
		||||
use App\Models\LeaveDays;
 | 
			
		||||
use App\Models\Notes;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
			
		||||
use Illuminate\Queue\InteractsWithQueue;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Facades\Log;
 | 
			
		||||
use Modules\Admin\app\Models\Category;
 | 
			
		||||
 | 
			
		||||
class UpdateTemporaryLeaveDays implements ShouldQueue
 | 
			
		||||
{
 | 
			
		||||
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 | 
			
		||||
    protected $month;
 | 
			
		||||
    protected $year;
 | 
			
		||||
 | 
			
		||||
    public function __construct($month = null, $year = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->month = $month ?? Carbon::now()->month;
 | 
			
		||||
        $this->year = $year ?? Carbon::now()->year;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the job.
 | 
			
		||||
     */
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        $users = User::get();
 | 
			
		||||
 | 
			
		||||
        foreach ($users as $user) {
 | 
			
		||||
            $leaveDay = LeaveDays::where('ld_user_id', $user->id)
 | 
			
		||||
                ->where('ld_year', $this->year)
 | 
			
		||||
                ->first();
 | 
			
		||||
 | 
			
		||||
            $notes = Notes::where('n_reason', 'TEMPORARY_ONLEAVE')
 | 
			
		||||
                ->where('n_user_id', $user->id)
 | 
			
		||||
                ->where('n_year', $this->year)
 | 
			
		||||
                ->where('n_month', $this->month)
 | 
			
		||||
                ->whereExists(function ($query) use ($user) {
 | 
			
		||||
                    $query->select(DB::raw(1))
 | 
			
		||||
                        ->from('tickets')
 | 
			
		||||
                        ->where('tickets.user_id', $user->id)
 | 
			
		||||
                        ->where('tickets.status', 'CONFIRMED')
 | 
			
		||||
                        ->where('tickets.type', 'ONLEAVE');
 | 
			
		||||
                })
 | 
			
		||||
                ->get();
 | 
			
		||||
 | 
			
		||||
            $maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
 | 
			
		||||
 | 
			
		||||
            // Tổng ngày nghỉ sẽ dùng trong tháng
 | 
			
		||||
            $willUsedDaysInMonth = 0;
 | 
			
		||||
            foreach ($notes as $note) {
 | 
			
		||||
                $willUsedDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Tổng phép đang có
 | 
			
		||||
            $onleaveDaysTotal = $leaveDay->ld_day_total +  $leaveDay->ld_additional_day + $leaveDay->ld_special_leave_day;
 | 
			
		||||
            // Phép đã sử dụng tới tháng hiện tại
 | 
			
		||||
            $usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
 | 
			
		||||
                $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
			
		||||
                    ->where('categories.c_type', 'TIME_TYPE');
 | 
			
		||||
            })
 | 
			
		||||
                ->where('n_user_id', $user->id)
 | 
			
		||||
                ->where('n_year', $this->year)
 | 
			
		||||
                ->where('n_month',  "<=", $this->month)
 | 
			
		||||
                ->where('n_reason', 'ONLEAVE')
 | 
			
		||||
                ->sum('categories.c_value');
 | 
			
		||||
            // Phép còn lại
 | 
			
		||||
            $remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal;
 | 
			
		||||
 | 
			
		||||
            // Log::debug("User {$user->name}\n");
 | 
			
		||||
            // Log::debug(
 | 
			
		||||
            //     "📊 Thống kê ngày phép:\n" .
 | 
			
		||||
            //         " - Tháng: {$this->month}\n" .
 | 
			
		||||
            //         " - Tổng ngày nghỉ sẽ dùng trong tháng: $willUsedDaysInMonth\n" .
 | 
			
		||||
            //         " - Tổng ngày phép: $onleaveDaysTotal\n" .
 | 
			
		||||
            //         " - Tổng ngày phép đã nghỉ: $usedOnleaveDaysTotal\n" .
 | 
			
		||||
            //         " - Tổng ngày phép còn lại: $remainingOnleaveDays\n"
 | 
			
		||||
            // );
 | 
			
		||||
 | 
			
		||||
            $onleave_days_will_use = 0; // Ngày phép sẽ dùng
 | 
			
		||||
            $nopay_days_will_use = 0;   // Ngày ko phép sẽ dùng
 | 
			
		||||
 | 
			
		||||
            // Ngày phép còn lại <= 0 (Hết phép)
 | 
			
		||||
            if ($remainingOnleaveDays <= 0) {
 | 
			
		||||
                $onleave_days_will_use = 0;
 | 
			
		||||
                $nopay_days_will_use = $willUsedDaysInMonth;
 | 
			
		||||
 | 
			
		||||
                Log::debug("--- Hết phép trong tháng ---");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
 | 
			
		||||
            else if ($remainingOnleaveDays < $willUsedDaysInMonth) {
 | 
			
		||||
                // Vượt limit
 | 
			
		||||
                if ($willUsedDaysInMonth > $maxDaysPerMonth) {
 | 
			
		||||
                    // Phép còn lại > limit
 | 
			
		||||
                    if ($remainingOnleaveDays > $maxDaysPerMonth) {
 | 
			
		||||
                        $onleave_days_will_use = $maxDaysPerMonth;
 | 
			
		||||
                        $nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
 | 
			
		||||
                    }
 | 
			
		||||
                    // Phép còn lại < limit
 | 
			
		||||
                    else {
 | 
			
		||||
                        $onleave_days_will_use = $remainingOnleaveDays;
 | 
			
		||||
                        $nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---",);
 | 
			
		||||
                }
 | 
			
		||||
                // Không vượt limit
 | 
			
		||||
                else {
 | 
			
		||||
                    $onleave_days_will_use = $remainingOnleaveDays;
 | 
			
		||||
                    $nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
 | 
			
		||||
 | 
			
		||||
                    Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Ngày phép còn lại >= ngày yêu cầu (Đủ phép)
 | 
			
		||||
            else {
 | 
			
		||||
                // Vượt limit
 | 
			
		||||
                if ($willUsedDaysInMonth > $maxDaysPerMonth) {
 | 
			
		||||
                    $onleave_days_will_use = $maxDaysPerMonth;
 | 
			
		||||
                    $nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
 | 
			
		||||
 | 
			
		||||
                    Log::debug("--- Đủ phép, vượt limit ---");
 | 
			
		||||
                }
 | 
			
		||||
                // Không vượt limit
 | 
			
		||||
                else {
 | 
			
		||||
                    $onleave_days_will_use = $willUsedDaysInMonth;
 | 
			
		||||
                    $nopay_days_will_use = 0;
 | 
			
		||||
 | 
			
		||||
                    Log::debug("--- Đủ phép ---");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log::debug("", [
 | 
			
		||||
                "Phep" => $onleave_days_will_use,
 | 
			
		||||
                "Khong Phep" => $nopay_days_will_use
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Có nghỉ không phép 
 | 
			
		||||
            if ($nopay_days_will_use > 0) {
 | 
			
		||||
                foreach ($notes as $note) {
 | 
			
		||||
                    $value = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
 | 
			
		||||
 | 
			
		||||
                    if ($note->n_time_type === 'ALL' && $onleave_days_will_use == 0.5) {
 | 
			
		||||
                        // Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép
 | 
			
		||||
                        // Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C)
 | 
			
		||||
 | 
			
		||||
                        Notes::create([
 | 
			
		||||
                            'n_user_id' => $user->id,
 | 
			
		||||
                            'n_day' => $note->n_day,
 | 
			
		||||
                            'n_month' => $note->n_month,
 | 
			
		||||
                            'n_year' => $note->n_year,
 | 
			
		||||
                            'n_time_type' => 'S',
 | 
			
		||||
                            'n_reason' => 'ONLEAVE',
 | 
			
		||||
                            'n_note' => $note->n_note,
 | 
			
		||||
                            'ticket_id' => $note->ticket_id
 | 
			
		||||
                        ]);
 | 
			
		||||
                        Notes::create([
 | 
			
		||||
                            'n_user_id' => $user->id,
 | 
			
		||||
                            'n_day' => $note->n_day,
 | 
			
		||||
                            'n_month' => $note->n_month,
 | 
			
		||||
                            'n_year' => $note->n_year,
 | 
			
		||||
                            'n_time_type' => 'C',
 | 
			
		||||
                            'n_reason' => 'LEAVE_WITHOUT_PAY',
 | 
			
		||||
                            'n_note' => $note->n_note,
 | 
			
		||||
                            'ticket_id' => $note->ticket_id
 | 
			
		||||
                        ]);
 | 
			
		||||
 | 
			
		||||
                        $note->delete();
 | 
			
		||||
 | 
			
		||||
                        $onleave_days_will_use = 0;
 | 
			
		||||
                        $nopay_days_will_use -= 0.5;
 | 
			
		||||
                    } elseif ($onleave_days_will_use > 0) {
 | 
			
		||||
                        // Dùng ngày phép trước
 | 
			
		||||
                        $use = min($onleave_days_will_use, $value);
 | 
			
		||||
                        $note->update([
 | 
			
		||||
                            'n_reason' => "ONLEAVE"
 | 
			
		||||
                        ]);
 | 
			
		||||
                        $onleave_days_will_use -= $use;
 | 
			
		||||
                    } elseif ($nopay_days_will_use > 0) {
 | 
			
		||||
                        // Hết phép, chuyển sang không phép
 | 
			
		||||
                        $use = min($nopay_days_will_use,  $value);
 | 
			
		||||
                        $note->update([
 | 
			
		||||
                            'n_reason' => "LEAVE_WITHOUT_PAY"
 | 
			
		||||
                        ]);
 | 
			
		||||
                        $nopay_days_will_use -= $use;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // Đủ phép
 | 
			
		||||
            else {
 | 
			
		||||
                foreach ($notes as $note) {
 | 
			
		||||
                    $note->update([
 | 
			
		||||
                        'n_reason' => "ONLEAVE"
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getMaxLeaveDaysPerMonth(): int
 | 
			
		||||
    {
 | 
			
		||||
        $limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first();
 | 
			
		||||
        if ($limitLeaveMonth) {
 | 
			
		||||
            $maxDaysPerMonth = (int)$limitLeaveMonth->c_value;
 | 
			
		||||
        } else {
 | 
			
		||||
            $maxDaysPerMonth = 3; // default nếu k có setting
 | 
			
		||||
        }
 | 
			
		||||
        return $maxDaysPerMonth;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ class LeaveDays extends Model
 | 
			
		|||
    use HasFactory;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = [
 | 
			
		||||
        'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note'
 | 
			
		||||
        'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    protected $table = 'leave_days';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,14 @@ class Notes extends Model
 | 
			
		|||
    use HasFactory;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = [
 | 
			
		||||
        'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
 | 
			
		||||
        'n_user_id',
 | 
			
		||||
        'n_day',
 | 
			
		||||
        'n_month',
 | 
			
		||||
        'n_year',
 | 
			
		||||
        'n_time_type',
 | 
			
		||||
        'n_reason',
 | 
			
		||||
        'n_note',
 | 
			
		||||
        'ticket_id'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +32,7 @@ class Notes extends Model
 | 
			
		|||
    {
 | 
			
		||||
        return self::leftJoin("categories as reason", function ($join) {
 | 
			
		||||
            $join->on('n_reason', '=', 'reason.c_code');
 | 
			
		||||
            $join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
 | 
			
		||||
            $join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
 | 
			
		||||
        })
 | 
			
		||||
            ->leftJoin("categories as timeTypes", function ($join) {
 | 
			
		||||
                $join->on('n_time_type', '=', 'timeTypes.c_code');
 | 
			
		||||
| 
						 | 
				
			
			@ -47,4 +54,18 @@ class Notes extends Model
 | 
			
		|||
            )
 | 
			
		||||
            ->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNotesByMonthAndYearAndUserId($month, $year, $userId, $idNote)
 | 
			
		||||
    {
 | 
			
		||||
        return self::where('n_reason', 'ONLEAVE')->where('n_month', $month)->where('n_year', $year)
 | 
			
		||||
            ->where('n_user_id', $userId)
 | 
			
		||||
            ->where('id', '!=', $idNote)->get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNotesByMonthAndYearAndUserIdAndReason($month, $year, $userId, $reason)
 | 
			
		||||
    {
 | 
			
		||||
        return self::where('n_reason', $reason)->where('n_month', $month)->where('n_year', $year)
 | 
			
		||||
            ->where('n_user_id', $userId)
 | 
			
		||||
            ->orderBy('n_day', 'asc')->orderBy('n_time_type', 'desc')->get();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,8 @@ return [
 | 
			
		|||
 | 
			
		||||
    'asset_url' => env('ASSET_URL'),
 | 
			
		||||
 | 
			
		||||
    'client_url' => env('ADMIN_URL', 'http://localhost'),
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    |--------------------------------------------------------------------------
 | 
			
		||||
    | Application Timezone
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
class RenameLdDayToLdDayTotalInLeaveDaysTable extends Migration
 | 
			
		||||
{
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->renameColumn('ld_day', 'ld_day_total');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->renameColumn('ld_day_total', 'ld_day');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
class AddLdSpecialLeaveDayToLeaveDaysTable extends Migration
 | 
			
		||||
{
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->float('ld_special_leave_day')->default(0); // Adding the new field
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->dropColumn('ld_special_leave_day'); // Dropping the field if needed
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
class RenameLdDateAdditionalToLdAdditionalDayInLeaveDaysTable extends Migration
 | 
			
		||||
{
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->renameColumn('ld_date_additional', 'ld_additional_day');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('leave_days', function (Blueprint $table) {
 | 
			
		||||
            $table->renameColumn('ld_date_additional', 'ld_additional_day');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
return new class extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function up(): void
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'LEAVE_WITHOUT_PAY',
 | 
			
		||||
                'c_name' => 'Nghỉ không hưởng lương',
 | 
			
		||||
                'c_type' => 'REASON',
 | 
			
		||||
                'c_value' => "",
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_name', 'Nghỉ phép')
 | 
			
		||||
            ->update(['c_name' => 'Nghỉ phép năm']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_name', 'Nghỉ phép năm')
 | 
			
		||||
            ->update(['c_name' => 'Nghỉ phép']);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
class UpdateLeaveCategories extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        // Xóa item với type REASON và code LEAVE_WITHOUT_PAY
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_type', 'REASON')
 | 
			
		||||
            ->where('c_code', 'LEAVE_WITHOUT_PAY')
 | 
			
		||||
            ->delete();
 | 
			
		||||
 | 
			
		||||
        // Cập nhật tên "Nghỉ phép năm" thành "Nghỉ phép"
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_name', 'Nghỉ phép năm')
 | 
			
		||||
            ->update(['c_name' => 'Nghỉ phép']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        // Khôi phục item đã xóa
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            'c_code' => 'LEAVE_WITHOUT_PAY',
 | 
			
		||||
            'c_name' => 'Không phép',
 | 
			
		||||
            'c_type' => 'REASON',
 | 
			
		||||
            'c_value' => "",
 | 
			
		||||
            'c_active' => 1,
 | 
			
		||||
            'created_at' => now(),
 | 
			
		||||
            'updated_at' => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Khôi phục tên cũ
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_name', 'Nghỉ phép')
 | 
			
		||||
            ->update(['c_name' => 'Nghỉ phép năm']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
class AddLimitLeaveMonthCategory extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            'c_code' => 'LIMIT',
 | 
			
		||||
            'c_name' => 'Giới hạn số ngày nghỉ có phép/tháng',
 | 
			
		||||
            'c_type' => 'LIMIT_LEAVE_MONTH',
 | 
			
		||||
            'c_value' => '3',
 | 
			
		||||
            'c_active' => 1,
 | 
			
		||||
            'created_at' => now(),
 | 
			
		||||
            'updated_at' => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_code', 'LIMIT')
 | 
			
		||||
            ->where('c_type', 'LIMIT_LEAVE_MONTH')
 | 
			
		||||
            ->delete();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
class AddSaturdayWorkScheduleCategory extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            'c_code' => '10-05-2025',
 | 
			
		||||
            'c_name' => 'Ngày bắt đầu làm việc thứ 7 trong năm',
 | 
			
		||||
            'c_type' => 'SATURDAY_WORK_SCHEDULE',
 | 
			
		||||
            'c_value' => '2025',
 | 
			
		||||
            'c_active' => 1,
 | 
			
		||||
            'created_at' => now(),
 | 
			
		||||
            'updated_at' => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_code', '10-05-2025')
 | 
			
		||||
            ->where('c_type', 'SATURDAY_WORK_SCHEDULE')
 | 
			
		||||
            ->delete();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
class AddDayWorkSpecialCategory extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            'c_code' => '17-05-2025',
 | 
			
		||||
            'c_name' => 'Ngày làm việc đặc biệt',
 | 
			
		||||
            'c_type' => 'DAY_WORK_SPECIAL',
 | 
			
		||||
            'c_value' => '2025',
 | 
			
		||||
            'c_active' => 1,
 | 
			
		||||
            'created_at' => now(),
 | 
			
		||||
            'updated_at' => now(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->where('c_code', '17-05-2025')
 | 
			
		||||
            ->where('c_type', 'DAY_WORK_SPECIAL')
 | 
			
		||||
            ->delete();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
 | 
			
		||||
class AddLeaveCategories extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'LEAVE_WITHOUT_PAY',
 | 
			
		||||
                'c_name' => 'Không phép',
 | 
			
		||||
                'c_type' => 'REASON_NOTES',
 | 
			
		||||
                'c_value' => "",
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'WFH',
 | 
			
		||||
                'c_name' => 'Work From Home',
 | 
			
		||||
                'c_type' => 'REASON_NOTES',
 | 
			
		||||
                'c_value' => "",
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'ONLEAVE',
 | 
			
		||||
                'c_name' => 'Nghỉ phép',
 | 
			
		||||
                'c_type' => 'REASON_NOTES',
 | 
			
		||||
                'c_value' => "",
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')
 | 
			
		||||
            ->whereIn('c_code', ['LEAVE_WITHOUT_PAY', 'WFH', 'ONLEAVE'])
 | 
			
		||||
            ->where('c_type', 'REASON_NOTES')
 | 
			
		||||
            ->delete();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
return new class extends Migration
 | 
			
		||||
{
 | 
			
		||||
    public function up(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('notes', function (Blueprint $table) {
 | 
			
		||||
            $table->foreignId('ticket_id')
 | 
			
		||||
                ->nullable()
 | 
			
		||||
                ->constrained('tickets')
 | 
			
		||||
                ->onDelete('cascade');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('notes', function (Blueprint $table) {
 | 
			
		||||
            $table->dropForeign(['ticket_id']);
 | 
			
		||||
            $table->dropColumn('ticket_id');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
<?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::table('users', function (Blueprint $table) {
 | 
			
		||||
            $table->boolean('is_permanent')->default(true);
 | 
			
		||||
            $table->date('permanent_date');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::table('users', function (Blueprint $table) {
 | 
			
		||||
            $table->dropColumn('is_permanent');
 | 
			
		||||
            $table->dropColumn('permanent_date');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
<?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
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->insert([
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'PERMANENT',
 | 
			
		||||
                'c_name' => 'Phép cộng nhân viên chính thức',
 | 
			
		||||
                'c_type' => 'PERMANENT_ONLEAVE',
 | 
			
		||||
                'c_value' => 1,
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                'c_code' => 'TEMPORARY_ONLEAVE',
 | 
			
		||||
                'c_name' => 'Nghỉ dự kiến',
 | 
			
		||||
                'c_type' => 'REASON_NOTES',
 | 
			
		||||
                'c_value' => "",
 | 
			
		||||
                'c_active' => 1,
 | 
			
		||||
                'created_at' => now(),
 | 
			
		||||
                'updated_at' => now(),
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        DB::table('categories')->where('c_code', 'PERMANENT')->where('c_type', 'PERMANENT_ONLEAVE')->delete();
 | 
			
		||||
        DB::table('categories')->where('c_code', 'TEMPORARY_ONLEAVE')->where('c_type', 'REASON_NOTES')->delete();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
 | 
			
		|||
        //     'name' => 'Test User',
 | 
			
		||||
        //     'email' => 'test@example.com',
 | 
			
		||||
        // ]);
 | 
			
		||||
 | 
			
		||||
        $this->call([
 | 
			
		||||
            UserSeeder::class,
 | 
			
		||||
            FileSeeder::class,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -37,8 +36,6 @@
 | 
			
		|||
    <title>{{ $data['subject'] }}</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
<body style="
 | 
			
		||||
        font-family: Arial, Helvetica, sans-serif;
 | 
			
		||||
        background-color: #edf2f7;
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +80,7 @@
 | 
			
		|||
                    <tr>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            <p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
 | 
			
		||||
                                    Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a request ticket, the specific content is as follows:
 | 
			
		||||
                                Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a <a href="{{ config('app.client_url') . $data['link'] }}"> request ticket</a>, the specific content is as follows:
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
| 
						 | 
				
			
			@ -98,15 +95,34 @@
 | 
			
		|||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>
 | 
			
		||||
                                <p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
 | 
			
		||||
                                    <a href='{{ config('app.url') . $data['link'] }}'
 | 
			
		||||
                            <p style="text-align: center">
 | 
			
		||||
                                You can quick
 | 
			
		||||
                                <span style="font-weight: bold">Confirm</span> or
 | 
			
		||||
                                <span style="font-weight: bold">Refuse</span> here:
 | 
			
		||||
                            </p>
 | 
			
		||||
 | 
			
		||||
                            <div
 | 
			
		||||
                                style="
 | 
			
		||||
                                        display: flex;
 | 
			
		||||
                                        justify-content: center;
 | 
			
		||||
                                        gap: 10px;
 | 
			
		||||
                                        margin-top: 10px;
 | 
			
		||||
                                    ">
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'confirm', 'admin_email' => $data['admin_email']]) }}"
 | 
			
		||||
                                    style="
 | 
			
		||||
                                            color: #fff;
 | 
			
		||||
                                            border-radius: 10px;
 | 
			
		||||
                                  background-color: rgba(68,115,196);
 | 
			
		||||
                                  background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
 | 
			
		||||
                                            background-color: #12b886;
 | 
			
		||||
                                            background-image: linear-gradient(
 | 
			
		||||
                                                to top left,
 | 
			
		||||
                                                rgba(0, 0, 0, 0.2),
 | 
			
		||||
                                                rgba(0, 0, 0, 0.2) 30%,
 | 
			
		||||
                                                rgba(0, 0, 0, 0)
 | 
			
		||||
                                            );
 | 
			
		||||
                                            text-decoration: none;
 | 
			
		||||
                                            display: inline-block;
 | 
			
		||||
                                            font-weight: 600;
 | 
			
		||||
| 
						 | 
				
			
			@ -116,11 +132,43 @@
 | 
			
		|||
                                            margin: 0;
 | 
			
		||||
                                            padding: 10px 12px;
 | 
			
		||||
                                        ">
 | 
			
		||||
                                        Check now</a>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                    Confirm</a>
 | 
			
		||||
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'refuse', 'admin_email' => $data['admin_email']]) }}"
 | 
			
		||||
                                    style="
 | 
			
		||||
                                            color: #fff;
 | 
			
		||||
                                            border-radius: 10px;
 | 
			
		||||
                                            background-color: #f03e3e;
 | 
			
		||||
                                            background-image: linear-gradient(
 | 
			
		||||
                                                to top left,
 | 
			
		||||
                                                rgba(0, 0, 0, 0.2),
 | 
			
		||||
                                                rgba(0, 0, 0, 0.2) 30%,
 | 
			
		||||
                                                rgba(0, 0, 0, 0)
 | 
			
		||||
                                            );
 | 
			
		||||
                                            text-decoration: none;
 | 
			
		||||
                                            display: inline-block;
 | 
			
		||||
                                            font-weight: 600;
 | 
			
		||||
                                            font-size: 16px;
 | 
			
		||||
                                            line-height: 150%;
 | 
			
		||||
                                            text-align: center;
 | 
			
		||||
                                            margin: 0;
 | 
			
		||||
                                            padding: 10px 12px;
 | 
			
		||||
                                        ">
 | 
			
		||||
                                    Refuse</a>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td style="color: #222222;">
 | 
			
		||||
                            <div style="margin-top: 3rem">
 | 
			
		||||
                                <p><span style="font-weight: bold">Note</span>: If you are redirected to a <span style="font-weight: bold">404 page</span>, it means:</p>
 | 
			
		||||
                                <p>1. The ticket has already been approved by another admin.</p>
 | 
			
		||||
                                <p>2. The ticket has been deleted.</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </table>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
| 
						 | 
				
			
			@ -150,6 +198,5 @@
 | 
			
		|||
        </tr>
 | 
			
		||||
    </table>
 | 
			
		||||
</body>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -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> 
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
 | 
			
		||||
require_once __DIR__ . "/../vendor/autoload.php";
 | 
			
		||||
$app = include_once __DIR__ . '/../bootstrap/app.php';
 | 
			
		||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
 | 
			
		||||
$kernel->bootstrap();
 | 
			
		||||
 | 
			
		||||
// Cập nhật lại data cho tới tháng hiện tại
 | 
			
		||||
$currentMonth = Carbon::now()->month;
 | 
			
		||||
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
 | 
			
		||||
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 124 KiB  | 
| 
						 | 
				
			
			@ -40,6 +40,8 @@ export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
 | 
			
		|||
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
 | 
			
		||||
export const updateNoteLeave =
 | 
			
		||||
  API_URL + 'v1/admin/leave-management/saveNoteLeave'
 | 
			
		||||
export const updateNoteStatus =
 | 
			
		||||
  API_URL + 'v1/admin/leave-management/updateNoteStatus'
 | 
			
		||||
export const exportLeaveManagement =
 | 
			
		||||
  API_URL + 'v1/admin/leave-management/export'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +50,7 @@ export const getTickets = API_URL + 'v1/admin/ticket/all'
 | 
			
		|||
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
 | 
			
		||||
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
 | 
			
		||||
export const addTicket = API_URL + 'v1/admin/ticket/create'
 | 
			
		||||
export const updateTicket = API_URL + 'v1/admin/ticket/update'
 | 
			
		||||
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
 | 
			
		||||
 | 
			
		||||
//Users
 | 
			
		||||
| 
						 | 
				
			
			@ -119,3 +122,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
 | 
			
		|||
 | 
			
		||||
// Download File
 | 
			
		||||
export const downloadFile = API_URL + 'v1/admin/download-file'
 | 
			
		||||
 | 
			
		||||
// Files APIs
 | 
			
		||||
export const getFiles = API_URL + 'v1/admin/profile/files'
 | 
			
		||||
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
 | 
			
		||||
export const deleteFileById = API_URL + 'v1/admin/profile/files'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,195 +1,95 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { getFiles, uploadFiles } from '@/api/Admin'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Card,
 | 
			
		||||
  Collapse,
 | 
			
		||||
  Group,
 | 
			
		||||
  Modal,
 | 
			
		||||
  RenderTreeNodePayload,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Tree,
 | 
			
		||||
  Title,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconChevronDown,
 | 
			
		||||
  IconDownload,
 | 
			
		||||
  IconFileTypeDocx,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFolder,
 | 
			
		||||
  IconFolderOpen,
 | 
			
		||||
  IconFolderX,
 | 
			
		||||
  IconListCheck,
 | 
			
		||||
  IconPhoto,
 | 
			
		||||
  IconSearch,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import FileUploadForm from '../Profile/components/FileUploadForm'
 | 
			
		||||
import classes from './AllProfiles.module.css'
 | 
			
		||||
 | 
			
		||||
interface FileIconProps {
 | 
			
		||||
interface FileData {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  isFolder: boolean
 | 
			
		||||
  expanded: boolean
 | 
			
		||||
  url: string
 | 
			
		||||
  type: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  created_at: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
interface GroupedFiles {
 | 
			
		||||
  [key: string]: FileData[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AllProfiles = () => {
 | 
			
		||||
  const [treeData, setTreeData] = useState([])
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
			
		||||
  const [currentName, setCurrentName] = useState<string>('')
 | 
			
		||||
  const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
 | 
			
		||||
  const [currentUser, setCurrentUser] = useState<string>('')
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  function FileIcon({ name, isFolder, expanded }: FileIconProps) {
 | 
			
		||||
    if (name.endsWith('.pdf')) {
 | 
			
		||||
      return <IconFileTypePdf size={14} />
 | 
			
		||||
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
  const [expandedFolders, setExpandedFolders] = useState<{
 | 
			
		||||
    [key: string]: boolean
 | 
			
		||||
  }>({})
 | 
			
		||||
  const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
 | 
			
		||||
 | 
			
		||||
  const toggleFolder = (userName: string) => {
 | 
			
		||||
    setExpandedFolders((prev) => ({
 | 
			
		||||
      ...prev,
 | 
			
		||||
      [userName]: !prev[userName],
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
			
		||||
      return <IconFileTypeDocx size={14} />
 | 
			
		||||
  const getFileIcon = (type: string) => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'document':
 | 
			
		||||
        return <IconFileTypeDocx size={16} />
 | 
			
		||||
      case 'image':
 | 
			
		||||
        return <IconPhoto size={16} />
 | 
			
		||||
      default:
 | 
			
		||||
        return <IconFileTypePdf size={16} />
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      name.endsWith('.jpg') ||
 | 
			
		||||
      name.endsWith('.png') ||
 | 
			
		||||
      name.endsWith('.jpeg') ||
 | 
			
		||||
      name.endsWith('.webp')
 | 
			
		||||
    ) {
 | 
			
		||||
      return <IconPhoto size={14} />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isFolder) {
 | 
			
		||||
      return expanded ? (
 | 
			
		||||
        <IconFolderOpen
 | 
			
		||||
          color="var(--mantine-color-yellow-9)"
 | 
			
		||||
          size={14}
 | 
			
		||||
          stroke={2.5}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <IconFolder
 | 
			
		||||
          color="var(--mantine-color-yellow-9)"
 | 
			
		||||
          size={14}
 | 
			
		||||
          stroke={2.5}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <IconFolderX color="var(--mantine-color-red-9)" size={14} stroke={2.5} />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function Leaf({
 | 
			
		||||
    node,
 | 
			
		||||
    expanded,
 | 
			
		||||
    hasChildren,
 | 
			
		||||
    elementProps,
 | 
			
		||||
  }: RenderTreeNodePayload) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Group gap={5} {...elementProps}>
 | 
			
		||||
        {!node.children ? (
 | 
			
		||||
          <a href={node.value} target="_blank">
 | 
			
		||||
            <FileIcon
 | 
			
		||||
              name={node.value}
 | 
			
		||||
              isFolder={hasChildren}
 | 
			
		||||
              expanded={expanded}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{node.label}</span>
 | 
			
		||||
          </a>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <>
 | 
			
		||||
            <FileIcon
 | 
			
		||||
              name={node.value}
 | 
			
		||||
              isFolder={hasChildren}
 | 
			
		||||
              expanded={expanded}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{node.label}</span>
 | 
			
		||||
            <Tooltip label="Upload">
 | 
			
		||||
              <IconListCheck
 | 
			
		||||
                color="green"
 | 
			
		||||
                width={15}
 | 
			
		||||
                height={15}
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: node.label !== 'others' ? 'block' : 'none',
 | 
			
		||||
                  cursor: 'pointer',
 | 
			
		||||
                  zIndex: 100000,
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setCurrentName(node.label!.toString())
 | 
			
		||||
                  setOpenedProfile(true)
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
      </Group>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  const handleSubmit = async (
 | 
			
		||||
    e: React.FormEvent,
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
    currentUser: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
      formData.append('file', selectedFile)
 | 
			
		||||
      formData.append('name', fileName)
 | 
			
		||||
      formData.append('description', description)
 | 
			
		||||
      formData.append('user_name', currentUser)
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    formData.append('name', currentName)
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
      try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -197,302 +97,234 @@ const AllProfiles = () => {
 | 
			
		|||
        })
 | 
			
		||||
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        getTree()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
          await getAllFiles()
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
        throw error
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
  const getAllFiles = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + currentName,
 | 
			
		||||
      })
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
        setGroupedFiles(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, { file_url: url }, getAllFile)
 | 
			
		||||
      getTree()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const getTree = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles',
 | 
			
		||||
      })
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setTreeData(res.data)
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFiles();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getTree()
 | 
			
		||||
    getAllFiles()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [currentName])
 | 
			
		||||
  const filterFiles = (files: FileData[], searchTerm: string) => {
 | 
			
		||||
    return files.filter(
 | 
			
		||||
      (file) =>
 | 
			
		||||
        file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
        (file.description &&
 | 
			
		||||
          file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          <Text>Admin/</Text>
 | 
			
		||||
          Profiles
 | 
			
		||||
          <Text size="sm">Admin/</Text>
 | 
			
		||||
          Files Management
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Box ml={'lg'}>
 | 
			
		||||
        <Tree
 | 
			
		||||
          classNames={classes}
 | 
			
		||||
          selectOnClick
 | 
			
		||||
          clearSelectionOnOutsideClick
 | 
			
		||||
          data={treeData}
 | 
			
		||||
          renderNode={(payload) => <Leaf {...payload} />}
 | 
			
		||||
      <Box ml={'md'}>
 | 
			
		||||
        <Stack gap="xs">
 | 
			
		||||
          {Object.entries(groupedFiles).map(([userName, files]) => (
 | 
			
		||||
            <Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
 | 
			
		||||
              <Group
 | 
			
		||||
                justify="space-between"
 | 
			
		||||
                mb="xs"
 | 
			
		||||
                gap="xs"
 | 
			
		||||
                onClick={() => toggleFolder(userName)}
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Group gap="xs">
 | 
			
		||||
                  <IconFolder size={18} color="var(--mantine-color-yellow-9)" />
 | 
			
		||||
                  <Title order={5}>{userName}</Title>
 | 
			
		||||
                </Group>
 | 
			
		||||
                <Group gap="xs">
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    variant="subtle"
 | 
			
		||||
                    color="gray"
 | 
			
		||||
                    onClick={() => toggleFolder(userName)}
 | 
			
		||||
                    leftSection={
 | 
			
		||||
                      <IconChevronDown
 | 
			
		||||
                        size={14}
 | 
			
		||||
                        style={{
 | 
			
		||||
                          transform: expandedFolders[userName]
 | 
			
		||||
                            ? 'rotate(180deg)'
 | 
			
		||||
                            : 'none',
 | 
			
		||||
                          transition: 'transform 0.2s',
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    {expandedFolders[userName] ? 'Collapse' : 'Expand'}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    variant="light"
 | 
			
		||||
                    color="blue"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setCurrentUser(userName)
 | 
			
		||||
                      setOpenedProfile(true)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Group gap={2}>
 | 
			
		||||
                      <IconListCheck size={14} />
 | 
			
		||||
                      <Text size="xs">Upload Files</Text>
 | 
			
		||||
                    </Group>
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Group>
 | 
			
		||||
              </Group>
 | 
			
		||||
              <Collapse in={expandedFolders[userName]}>
 | 
			
		||||
                <Stack gap="xs">
 | 
			
		||||
                  <TextInput
 | 
			
		||||
                    placeholder="Search files by name or description..."
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    leftSection={<IconSearch size={14} />}
 | 
			
		||||
                    value={searchTerms[userName] || ''}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                      setSearchTerms((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [userName]: e.target.value,
 | 
			
		||||
                      }))
 | 
			
		||||
                    }
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                  />
 | 
			
		||||
                  {filterFiles(files, searchTerms[userName] || '')
 | 
			
		||||
                    .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
 | 
			
		||||
                    .map((file: FileData) => (
 | 
			
		||||
                      <Card
 | 
			
		||||
                        key={file.id}
 | 
			
		||||
                        shadow="xs"
 | 
			
		||||
                        padding="xs"
 | 
			
		||||
                        radius="sm"
 | 
			
		||||
                        withBorder
 | 
			
		||||
                      >
 | 
			
		||||
                        <Group justify="space-between" gap="xs">
 | 
			
		||||
                          <Group gap="xs">
 | 
			
		||||
                            {getFileIcon(file.type)}
 | 
			
		||||
                            <Box>
 | 
			
		||||
                              <Text size="xs" fw={500}>
 | 
			
		||||
                                {file.name}
 | 
			
		||||
                              </Text>
 | 
			
		||||
                              {file.description && (
 | 
			
		||||
                                <Text size="xs" c="dimmed">
 | 
			
		||||
                                  {file.description}
 | 
			
		||||
                                </Text>
 | 
			
		||||
                              )}
 | 
			
		||||
                              <Text size="xs" c="dimmed">
 | 
			
		||||
                                Uploaded:{' '}
 | 
			
		||||
                                {new Date(file.created_at).toLocaleDateString()}
 | 
			
		||||
                              </Text>
 | 
			
		||||
                            </Box>
 | 
			
		||||
                          </Group>
 | 
			
		||||
                          <Group gap="xs">
 | 
			
		||||
                            <Button
 | 
			
		||||
                              size="xs"
 | 
			
		||||
                              variant="light" 
 | 
			
		||||
                              color="blue"
 | 
			
		||||
                              component='a'
 | 
			
		||||
                              href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
                                import.meta.env.VITE_BACKEND_URL?.includes(
 | 
			
		||||
                                  'localhost',
 | 
			
		||||
                                )
 | 
			
		||||
                                  ? ''
 | 
			
		||||
                                  : 'image/'
 | 
			
		||||
                              }${file.url.slice(1)}`}
 | 
			
		||||
                              target="_blank"
 | 
			
		||||
                            >
 | 
			
		||||
                              <Group gap={2}>
 | 
			
		||||
                                <IconDownload size={12} />
 | 
			
		||||
                                <Text size="xs">Download</Text>
 | 
			
		||||
                              </Group>
 | 
			
		||||
                            </Button>
 | 
			
		||||
                            <Button
 | 
			
		||||
                              size="xs"
 | 
			
		||||
                              variant="light"
 | 
			
		||||
                              color="red"
 | 
			
		||||
                              onClick={() => removeFile(file.id)}
 | 
			
		||||
                            >
 | 
			
		||||
                              <Group gap={2}>
 | 
			
		||||
                                <IconTrash size={12} />
 | 
			
		||||
                                <Text size="xs">Delete</Text>
 | 
			
		||||
                              </Group>
 | 
			
		||||
                            </Button>
 | 
			
		||||
                          </Group>
 | 
			
		||||
                        </Group>
 | 
			
		||||
                      </Card>
 | 
			
		||||
                    ),
 | 
			
		||||
                  )}
 | 
			
		||||
                </Stack>
 | 
			
		||||
              </Collapse>
 | 
			
		||||
            </Card>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Stack>
 | 
			
		||||
 | 
			
		||||
        <Modal
 | 
			
		||||
          size={'lg'}
 | 
			
		||||
          size="lg"
 | 
			
		||||
          opened={openedProfile}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setOpenedProfile(false)
 | 
			
		||||
            setCurrentUser('')
 | 
			
		||||
            setSelectedFile(null)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('cv')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label={'CV'}
 | 
			
		||||
                  placeholder="Tải lên CV"
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    0
 | 
			
		||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.doc,.docx"
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={groupedFiles[currentUser] || []}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={currentUser}
 | 
			
		||||
            />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CCCD</Text>
 | 
			
		||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('idCard')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="CCCD"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'none' : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên CCCD"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bảng điểm</Text>
 | 
			
		||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('transcript')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bảng điểm"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'none' : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên bảng điểm"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'flex'
 | 
			
		||||
                      : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bằng đại học</Text>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text>{`: ${
 | 
			
		||||
                      checkFileExist('universityDiploma')?.label
 | 
			
		||||
                    }`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('universityDiploma')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bằng đại học"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'none'
 | 
			
		||||
                      : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên bằng đại học"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setUniversityDiploma(
 | 
			
		||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
			
		||||
                    )
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Text>Danh sách file khác:</Text>
 | 
			
		||||
                <Box>
 | 
			
		||||
                  {data
 | 
			
		||||
                    .find((f) => f.label === 'others')
 | 
			
		||||
                    ?.children?.map((c, index) => {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Box
 | 
			
		||||
                          key={index}
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            marginBottom: '5px',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <a href={c?.value} target="_blank">
 | 
			
		||||
                            <Text>{`${c?.label}`}</Text>
 | 
			
		||||
                          </a>
 | 
			
		||||
                          <Button
 | 
			
		||||
                            variant="outline"
 | 
			
		||||
                            size="xs"
 | 
			
		||||
                            color="red"
 | 
			
		||||
                            ml={'sm'}
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                              removeFile(c?.value!)
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            Delete
 | 
			
		||||
                          </Button>
 | 
			
		||||
                        </Box>
 | 
			
		||||
                      )
 | 
			
		||||
                    })}
 | 
			
		||||
                </Box>
 | 
			
		||||
                {otherFiles.map((fileInput, index) => (
 | 
			
		||||
                  <Group key={index}>
 | 
			
		||||
                    <FileInput
 | 
			
		||||
                      placeholder="Chọn file"
 | 
			
		||||
                      onChange={(file) =>
 | 
			
		||||
                        handleOtherFileChange(index, 'file', file!)
 | 
			
		||||
                      }
 | 
			
		||||
                      w={'30%'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <TextInput
 | 
			
		||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
			
		||||
                      value={fileInput.type}
 | 
			
		||||
                      w={'65%'}
 | 
			
		||||
                      onChange={(e) =>
 | 
			
		||||
                        handleOtherFileChange(
 | 
			
		||||
                          index,
 | 
			
		||||
                          'type',
 | 
			
		||||
                          e.currentTarget.value,
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Group>
 | 
			
		||||
                ))}
 | 
			
		||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
			
		||||
                  Thêm file khác
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button type="submit" color="blue">
 | 
			
		||||
                  Save
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,3 +60,37 @@
 | 
			
		|||
  padding-top: 5px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Thêm styles cho Modal xác nhận xóa */
 | 
			
		||||
.deleteModal {
 | 
			
		||||
  background-color: light-dark(white, #2d353c);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  border: solid 1px #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalTitle {
 | 
			
		||||
  color: #ff4646;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalContent {
 | 
			
		||||
  color: light-dark(#2d353c, white);
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalFooter {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton {
 | 
			
		||||
  background-color: #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton:hover {
 | 
			
		||||
  background-color: #ff6b6b;
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,5 +1,42 @@
 | 
			
		|||
export const PageNotFound = () => {
 | 
			
		||||
  return <>{'Not found!'}</>
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Container,
 | 
			
		||||
  Image,
 | 
			
		||||
  SimpleGrid,
 | 
			
		||||
  Text,
 | 
			
		||||
  Title,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import image404 from '../../../public/404Image.jpg'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
const PageNotFound = () => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container mt="lg">
 | 
			
		||||
      <SimpleGrid spacing={{ base: 40, sm: 80 }} cols={{ base: 1, sm: 2 }}>
 | 
			
		||||
        <Box py="3rem">
 | 
			
		||||
          <Title> Something is not right...</Title>
 | 
			
		||||
          <Text c="dimmed" size="lg">
 | 
			
		||||
            Page you are trying to open does not exist. You may have mistyped
 | 
			
		||||
            the address, or the page has been moved to another URL. If you think
 | 
			
		||||
            this is an error contact support.
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            size="md"
 | 
			
		||||
            mt="xl"
 | 
			
		||||
            onClick={() => navigate('/')}
 | 
			
		||||
          >
 | 
			
		||||
            Get back to home page
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
 | 
			
		||||
        <Image src={image404} />
 | 
			
		||||
      </SimpleGrid>
 | 
			
		||||
    </Container>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PageNotFound
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,60 +148,44 @@ const Profile = () => {
 | 
			
		|||
    return []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  // const [cv, setCv] = useState<File>()
 | 
			
		||||
  // const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  // const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  // const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  // const handleOtherFileChange = (
 | 
			
		||||
  //   index: number,
 | 
			
		||||
  //   field: string,
 | 
			
		||||
  //   value: File | string,
 | 
			
		||||
  // ) => {
 | 
			
		||||
  //   const updatedFiles: any = [...otherFiles]
 | 
			
		||||
  //   updatedFiles[index][field] = value
 | 
			
		||||
  //   setOtherFiles(updatedFiles)
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // const addOtherFileInput = () => {
 | 
			
		||||
  //   setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (
 | 
			
		||||
    e: React.FormEvent,
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
      formData.append('file', selectedFile)
 | 
			
		||||
      formData.append('name', fileName)
 | 
			
		||||
      formData.append('description', description)
 | 
			
		||||
      formData.append('user_name', user.user.name)
 | 
			
		||||
      formData.append('user_id', user.user.id.toString())
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
      try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -198,41 +193,68 @@ const Profile = () => {
 | 
			
		|||
        })
 | 
			
		||||
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
          await getAllFile()
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
			
		||||
      })
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
        const userFiles = res.data[user.user.name] || [];
 | 
			
		||||
        setData(userFiles);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể tải danh sách file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, {file_url: url}, getAllFile)
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFile();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      const result = await getListProfilesData()
 | 
			
		||||
      setDataProfile(result ?? [])
 | 
			
		||||
      await getAllFile()
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleChangePassword = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -306,23 +328,19 @@ const Profile = () => {
 | 
			
		|||
    dispatch(logout(navigate))
 | 
			
		||||
  }, [dispatch, navigate])
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
  // const handleChangeFileName = (e: File, newName: string): File => {
 | 
			
		||||
  //   const originalFile = e;
 | 
			
		||||
  //   const extend = originalFile.name.split('.')[1];
 | 
			
		||||
  //   const newFileName = `${newName}.${extend}`;
 | 
			
		||||
  //   return new File([originalFile], newFileName, {
 | 
			
		||||
  //     type: originalFile.type,
 | 
			
		||||
  //   });
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
  // const checkFileExist = (nameField: string) => {
 | 
			
		||||
  //   const file = data.find((f) => f.name.includes(nameField));
 | 
			
		||||
  //   return file;
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
| 
						 | 
				
			
			@ -578,162 +596,14 @@ const Profile = () => {
 | 
			
		|||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('cv')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label={'CV'}
 | 
			
		||||
                  placeholder="Tải lên CV"
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    0
 | 
			
		||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.doc,.docx"
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={data}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={user.user.name}
 | 
			
		||||
            />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CCCD</Text>
 | 
			
		||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('idCard')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="CCCD"
 | 
			
		||||
                  style={{ display: checkFileExist('idCard') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên CCCD"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bảng điểm</Text>
 | 
			
		||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('transcript')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bảng điểm"
 | 
			
		||||
                  style={{ display: checkFileExist('transcript') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên bảng điểm"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'flex'
 | 
			
		||||
                      : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bằng đại học</Text>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text>{`: ${
 | 
			
		||||
                      checkFileExist('universityDiploma')?.label
 | 
			
		||||
                    }`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('universityDiploma')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bằng đại học"
 | 
			
		||||
                  style={{ display: checkFileExist('universityDiploma') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên bằng đại học"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setUniversityDiploma(
 | 
			
		||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
			
		||||
                    )
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Text>Danh sách file khác:</Text>
 | 
			
		||||
                  <Box>
 | 
			
		||||
                    {data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{
 | 
			
		||||
                      return <Box key={index}
 | 
			
		||||
                      style={{
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        marginBottom:'5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={c?.value}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                      >
 | 
			
		||||
                        <Text>{`${
 | 
			
		||||
                          c?.label
 | 
			
		||||
                        }`}</Text>
 | 
			
		||||
                      </a>
 | 
			
		||||
                      <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(c?.value!)}}>
 | 
			
		||||
                        Delete
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                    })}
 | 
			
		||||
                  </Box>
 | 
			
		||||
                {otherFiles.map((fileInput, index) => (
 | 
			
		||||
                  <Group key={index}>
 | 
			
		||||
                    <FileInput
 | 
			
		||||
                      placeholder="Chọn file"
 | 
			
		||||
                      onChange={(file) =>
 | 
			
		||||
                        handleOtherFileChange(index, 'file', file!)
 | 
			
		||||
                      }
 | 
			
		||||
                      w={'30%'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <TextInput
 | 
			
		||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
			
		||||
                      value={fileInput.type}
 | 
			
		||||
                      w={'65%'}
 | 
			
		||||
                      onChange={(e) =>
 | 
			
		||||
                        handleOtherFileChange(
 | 
			
		||||
                          index,
 | 
			
		||||
                          'type',
 | 
			
		||||
                          e.currentTarget.value,
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Group>
 | 
			
		||||
                ))}
 | 
			
		||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
			
		||||
                  Thêm file khác
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button type="submit" color="blue">
 | 
			
		||||
                  Save
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
.fileContainer {
 | 
			
		||||
    padding: 12px;
 | 
			
		||||
    margin: 8px 0;
 | 
			
		||||
    border: 1px solid #e9ecef;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    transition: all 0.2s ease;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileContainer:hover {
 | 
			
		||||
  background-color: #f8f9fa;
 | 
			
		||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileHeader {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileName {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileDescription {
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  color: #666;
 | 
			
		||||
  margin: 2px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileActions {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  margin-top: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileLink {
 | 
			
		||||
  color: #228be6;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  transition: color 0.2s ease;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  background-color: #e7f5ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileLink:hover {
 | 
			
		||||
  color: #1c7ed6;
 | 
			
		||||
  background-color: #d0ebff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton {
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileInputGroup {
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  margin: 16px 0;
 | 
			
		||||
  border: 2px dashed #e9ecef;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  background-color: #f8f9fa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fileInput,
 | 
			
		||||
.fileNameInput,
 | 
			
		||||
.descriptionInput {
 | 
			
		||||
  margin-bottom: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.saveButton {
 | 
			
		||||
  margin-top: 16px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 180px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.saveButton:disabled {
 | 
			
		||||
  background-color: #e9ecef;
 | 
			
		||||
  cursor: not-allowed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadingOverlay {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.8);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loadingSpinner {
 | 
			
		||||
  width: 32px;
 | 
			
		||||
  height: 32px;
 | 
			
		||||
  border: 3px solid #f3f3f3;
 | 
			
		||||
  border-top: 3px solid #3498db;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sectionTitle {
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  margin-bottom: 12px;
 | 
			
		||||
  color: #343a40;
 | 
			
		||||
  padding-bottom: 8px;
 | 
			
		||||
  border-bottom: 1px solid #e9ecef;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% { transform: rotate(0deg); }
 | 
			
		||||
  100% { transform: rotate(360deg); }
 | 
			
		||||
} 
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,251 @@
 | 
			
		|||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Card,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Group,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Textarea,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconDownload,
 | 
			
		||||
  IconFileTypeDocx,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFileTypeXls,
 | 
			
		||||
  IconPhoto,
 | 
			
		||||
  IconSearch,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import classes from './FileUploadForm.module.css'
 | 
			
		||||
 | 
			
		||||
// type TFileProfile = {
 | 
			
		||||
//   label: string
 | 
			
		||||
//   type: string
 | 
			
		||||
//   value: string
 | 
			
		||||
//   children?: TFileProfile[]
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
interface FileData {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  url: string
 | 
			
		||||
  type: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  created_at: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileUploadFormProps = {
 | 
			
		||||
  data: FileData[];
 | 
			
		||||
  handleSubmit: (e: React.FormEvent, fileName: string, description: string, currentUser: string) => Promise<boolean | void>;
 | 
			
		||||
  handleFileChange: (file: File | null) => void;
 | 
			
		||||
  removeFile: (id: number) => Promise<void>;
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
  currentUser: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FileUploadForm = ({
 | 
			
		||||
  data,
 | 
			
		||||
  handleSubmit,
 | 
			
		||||
  handleFileChange,
 | 
			
		||||
  removeFile,
 | 
			
		||||
  isLoading,
 | 
			
		||||
  currentUser,
 | 
			
		||||
}: FileUploadFormProps) => {
 | 
			
		||||
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
			
		||||
  const [fileName, setFileName] = useState('')
 | 
			
		||||
  const [description, setDescription] = useState('')
 | 
			
		||||
  const [isUploading, setIsUploading] = useState(false)
 | 
			
		||||
  const [searchTerm, setSearchTerm] = useState('')
 | 
			
		||||
 | 
			
		||||
  const handleFileSelect = (file: File | null) => {
 | 
			
		||||
    setSelectedFile(file)
 | 
			
		||||
    handleFileChange(file)
 | 
			
		||||
    if (file) {
 | 
			
		||||
      // Set default name as file name without extension
 | 
			
		||||
      setFileName(file.name.split('.')[0])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleFormSubmit = async (e: React.FormEvent) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsUploading(true)
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      await handleSubmit(e, fileName, description, currentUser)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Thành công',
 | 
			
		||||
        message: 'Tải file lên thành công',
 | 
			
		||||
        color: 'green',
 | 
			
		||||
      })
 | 
			
		||||
      setFileName('')
 | 
			
		||||
      setDescription('')
 | 
			
		||||
      setSelectedFile(null)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error submitting form:', error)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể tải file lên',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsUploading(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getFileIcon = (type: string) => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'document':
 | 
			
		||||
        return <IconFileTypeDocx size={16} />
 | 
			
		||||
      case 'image':
 | 
			
		||||
        return <IconPhoto size={16} />
 | 
			
		||||
      case 'spreadsheet':
 | 
			
		||||
        return <IconFileTypeXls size={16} />
 | 
			
		||||
      default:
 | 
			
		||||
        return <IconFileTypePdf size={16} />
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const filteredFiles = data.filter(
 | 
			
		||||
    (file) =>
 | 
			
		||||
      file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
      (file.description &&
 | 
			
		||||
        file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {isLoading && (
 | 
			
		||||
        <div className={classes.loadingOverlay}>
 | 
			
		||||
          <div className={classes.loadingSpinner} />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <form onSubmit={handleFormSubmit}>
 | 
			
		||||
        <Box>
 | 
			
		||||
          <Text className={classes.sectionTitle}>Tài liệu</Text>
 | 
			
		||||
 | 
			
		||||
          <Box className={classes.fileInputGroup}>
 | 
			
		||||
            <FileInput
 | 
			
		||||
              label="Chọn file"
 | 
			
		||||
              placeholder="Chọn file để tải lên"
 | 
			
		||||
              accept="image/png,image/jpeg,image/jpg,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet"
 | 
			
		||||
              onChange={handleFileSelect}
 | 
			
		||||
              value={selectedFile}
 | 
			
		||||
              className={classes.fileInput}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <TextInput
 | 
			
		||||
              label="Tên file"
 | 
			
		||||
              placeholder="Nhập tên file"
 | 
			
		||||
              value={fileName}
 | 
			
		||||
              onChange={(e) => setFileName(e.target.value)}
 | 
			
		||||
              className={classes.fileNameInput}
 | 
			
		||||
              required
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <Textarea
 | 
			
		||||
              label="Mô tả"
 | 
			
		||||
              placeholder="Nhập mô tả cho file"
 | 
			
		||||
              value={description}
 | 
			
		||||
              onChange={(e) => setDescription(e.target.value)}
 | 
			
		||||
              className={classes.descriptionInput}
 | 
			
		||||
              minRows={3}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              type="submit"
 | 
			
		||||
              color="blue"
 | 
			
		||||
              className={classes.saveButton}
 | 
			
		||||
              disabled={isLoading || isUploading || !selectedFile || !fileName}
 | 
			
		||||
              loading={isLoading || isUploading}
 | 
			
		||||
            >
 | 
			
		||||
              {isLoading || isUploading ? 'Đang xử lý...' : 'Lưu thay đổi'}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
 | 
			
		||||
          <Box className={classes.fileListContainer}>
 | 
			
		||||
            <TextInput
 | 
			
		||||
              placeholder="Tìm kiếm theo tên hoặc mô tả..."
 | 
			
		||||
              leftSection={<IconSearch size={14} />}
 | 
			
		||||
              value={searchTerm}
 | 
			
		||||
              onChange={(e) => setSearchTerm(e.target.value)}
 | 
			
		||||
              className={classes.searchInput}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <Stack className={classes.fileList} gap="xs">
 | 
			
		||||
              {filteredFiles.map((file) => (
 | 
			
		||||
                <Card
 | 
			
		||||
                  key={file.id}
 | 
			
		||||
                  shadow="xs"
 | 
			
		||||
                  padding="xs"
 | 
			
		||||
                  radius="sm"
 | 
			
		||||
                  withBorder
 | 
			
		||||
                >
 | 
			
		||||
                  <Group justify="space-between" gap="xs" wrap="nowrap">
 | 
			
		||||
                    <Group gap="xs" className={classes.cardContent}>
 | 
			
		||||
                      {getFileIcon(file.type)}
 | 
			
		||||
                      <Box style={{ minWidth: 0 }}>
 | 
			
		||||
                        <Text size="xs" fw={500} className={classes.cardTitle}>
 | 
			
		||||
                          {file.name}
 | 
			
		||||
                        </Text>
 | 
			
		||||
                        {file.description && (
 | 
			
		||||
                          <Text
 | 
			
		||||
                            size="xs"
 | 
			
		||||
                            c="dimmed"
 | 
			
		||||
                            className={classes.cardDescription}
 | 
			
		||||
                          >
 | 
			
		||||
                            {file.description}
 | 
			
		||||
                          </Text>
 | 
			
		||||
                        )}
 | 
			
		||||
                        <Text size="xs" c="dimmed">
 | 
			
		||||
                          Uploaded:{' '}
 | 
			
		||||
                          {new Date(file.created_at).toLocaleDateString()}
 | 
			
		||||
                        </Text>
 | 
			
		||||
                      </Box>
 | 
			
		||||
                    </Group>
 | 
			
		||||
                    <Group gap="xs" wrap="nowrap">
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size="xs"
 | 
			
		||||
                        variant="light"
 | 
			
		||||
                        color="blue"
 | 
			
		||||
                        component="a"
 | 
			
		||||
                        href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
                          import.meta.env.VITE_BACKEND_URL?.includes(
 | 
			
		||||
                            'localhost',
 | 
			
		||||
                          )
 | 
			
		||||
                            ? ''
 | 
			
		||||
                            : 'image/'
 | 
			
		||||
                        }${file.url.slice(1)}`}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                      >
 | 
			
		||||
                        <Group gap={2}>
 | 
			
		||||
                          <IconDownload size={12} />
 | 
			
		||||
                        </Group>
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size="xs"
 | 
			
		||||
                        variant="light"
 | 
			
		||||
                        color="red"
 | 
			
		||||
                        onClick={() => removeFile(file.id)}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Group gap={2}>
 | 
			
		||||
                          <IconTrash size={12} />
 | 
			
		||||
                        </Group>
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Group>
 | 
			
		||||
                  </Group>
 | 
			
		||||
                </Card>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Stack>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </form>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default FileUploadForm
 | 
			
		||||
| 
						 | 
				
			
			@ -5,8 +5,8 @@ import {
 | 
			
		|||
  getTicketsOfUser,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { DataTablePagination } from '@/components/DataTable/DataTable'
 | 
			
		||||
import { Xdelete, create } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get, post } from '@/rtk/helpers/apiService'
 | 
			
		||||
import {
 | 
			
		||||
  Badge,
 | 
			
		||||
  Box,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ import { IconTrash } from '@tabler/icons-react'
 | 
			
		|||
import moment from 'moment'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import classes from './Tickets.module.css'
 | 
			
		||||
import { _NOTIFICATION_MESS } from '@/rtk/helpers/notificationMess'
 | 
			
		||||
 | 
			
		||||
type TTickets = {
 | 
			
		||||
  id: number
 | 
			
		||||
| 
						 | 
				
			
			@ -78,10 +79,16 @@ const Tickets = () => {
 | 
			
		|||
  const [item, setItem] = useState({ id: 0 })
 | 
			
		||||
  const [activeBtn, setActiveBtn] = useState(false)
 | 
			
		||||
  const [disableBtn, setDisableBtn] = useState(false)
 | 
			
		||||
  const [isFetchData, setIsFetch] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
 | 
			
		||||
  const [dataReason, setDataReason] = useState<DataReason[]>([])
 | 
			
		||||
 | 
			
		||||
  const [confirmModal, setConfirmModal] = useState(false)
 | 
			
		||||
  const [confirmMessage, setConfirmMessage] = useState('')
 | 
			
		||||
  const [confirmValues, setConfirmValues] = useState<TTickets | null>(null)
 | 
			
		||||
  const [confirmLoading, setConfirmLoading] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const getListMasterByType = async (type: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const params = {
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +110,8 @@ const Tickets = () => {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      setIsFetch(true)
 | 
			
		||||
 | 
			
		||||
      const resultTimeType = await getListMasterByType('TIME_TYPE')
 | 
			
		||||
      setDataTimeType(
 | 
			
		||||
        resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +119,8 @@ const Tickets = () => {
 | 
			
		|||
 | 
			
		||||
      const resultReason = await getListMasterByType('REASON')
 | 
			
		||||
      setDataReason(resultReason)
 | 
			
		||||
 | 
			
		||||
      setIsFetch(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fetchData()
 | 
			
		||||
| 
						 | 
				
			
			@ -271,11 +282,9 @@ const Tickets = () => {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleCreate = async (values: TTickets) => {
 | 
			
		||||
  const handleCreate = async (values: TTickets, confirm: boolean = false) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await create(
 | 
			
		||||
        addTicket,
 | 
			
		||||
        {
 | 
			
		||||
      let params = {
 | 
			
		||||
        // time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
 | 
			
		||||
        start_date: moment(values.start_date).format('YYYY-MM-DD'),
 | 
			
		||||
        start_period: values.start_period,
 | 
			
		||||
| 
						 | 
				
			
			@ -283,15 +292,51 @@ const Tickets = () => {
 | 
			
		|||
        end_period: values.end_period,
 | 
			
		||||
        type: values.type,
 | 
			
		||||
        reason: values.reason,
 | 
			
		||||
        },
 | 
			
		||||
        getAllTickets,
 | 
			
		||||
      )
 | 
			
		||||
      if (res === true) {
 | 
			
		||||
        is_accept: confirm,
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let res = await post(addTicket, params)
 | 
			
		||||
 | 
			
		||||
      if (res.status) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Success',
 | 
			
		||||
          message: _NOTIFICATION_MESS.create_success,
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        })
 | 
			
		||||
        setAction('')
 | 
			
		||||
        form.reset()
 | 
			
		||||
 | 
			
		||||
        getAllTickets()
 | 
			
		||||
      }
 | 
			
		||||
      if (!res.status && res.errors) {
 | 
			
		||||
        if (!res.data?.success && res.data?.message) {
 | 
			
		||||
          //popup notification confirm or cancel
 | 
			
		||||
          setConfirmMessage(res.data?.message)
 | 
			
		||||
          setConfirmValues(values)
 | 
			
		||||
          setConfirmModal(true)
 | 
			
		||||
        } else {
 | 
			
		||||
          notifications.show({
 | 
			
		||||
            title: 'Error',
 | 
			
		||||
            message: (
 | 
			
		||||
              <div style={{ whiteSpace: 'pre-line' }}>
 | 
			
		||||
                {res.message ?? _NOTIFICATION_MESS.create_error}
 | 
			
		||||
              </div>
 | 
			
		||||
            ),
 | 
			
		||||
            color: 'red',
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      console.log(res, 'res')
 | 
			
		||||
      // return res.status
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      if (error.response.status === 422) {
 | 
			
		||||
        const errorMess = error.response.data.message
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Error',
 | 
			
		||||
          message: errorMess,
 | 
			
		||||
          color: 'red',
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -322,15 +367,14 @@ const Tickets = () => {
 | 
			
		|||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          Tickets
 | 
			
		||||
        </h3>
 | 
			
		||||
        <h3>Tickets</h3>
 | 
			
		||||
        <Button
 | 
			
		||||
          m={5}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setAction('add')
 | 
			
		||||
            form.reset()
 | 
			
		||||
          }}
 | 
			
		||||
          disabled={isFetchData}
 | 
			
		||||
        >
 | 
			
		||||
          + Add
 | 
			
		||||
        </Button>
 | 
			
		||||
| 
						 | 
				
			
			@ -490,6 +534,57 @@ const Tickets = () => {
 | 
			
		|||
          </Group>
 | 
			
		||||
        </Text>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
 | 
			
		||||
      {/* Confirm Modal */}
 | 
			
		||||
      <Modal
 | 
			
		||||
        opened={confirmModal}
 | 
			
		||||
        onClose={() => !confirmLoading && setConfirmModal(false)}
 | 
			
		||||
        title={
 | 
			
		||||
          <Text fw={700} fz="lg">
 | 
			
		||||
            Warning
 | 
			
		||||
          </Text>
 | 
			
		||||
        }
 | 
			
		||||
        centered
 | 
			
		||||
        closeOnClickOutside={!confirmLoading}
 | 
			
		||||
        closeOnEscape={!confirmLoading}
 | 
			
		||||
      >
 | 
			
		||||
        <Box p="md">
 | 
			
		||||
          <Text style={{ whiteSpace: 'pre-line' }} mb={20}>
 | 
			
		||||
            {confirmMessage}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Group justify="center">
 | 
			
		||||
            <Button
 | 
			
		||||
              color="green"
 | 
			
		||||
              loading={confirmLoading}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                if (confirmValues) {
 | 
			
		||||
                  try {
 | 
			
		||||
                    setConfirmLoading(true)
 | 
			
		||||
                    action === 'add' &&
 | 
			
		||||
                      (await handleCreate(confirmValues, true))
 | 
			
		||||
                    setConfirmLoading(false)
 | 
			
		||||
                    setConfirmModal(false)
 | 
			
		||||
                  } catch (error) {
 | 
			
		||||
                    setConfirmLoading(false)
 | 
			
		||||
                    console.error(error)
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Confirm
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              color="red"
 | 
			
		||||
              disabled={confirmLoading}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setConfirmModal(false)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Cancel
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Group>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,3 +46,37 @@
 | 
			
		|||
  .dialogText {
 | 
			
		||||
    color: light-dark(#2d353c, white);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* Thêm styles cho Modal xác nhận xóa */
 | 
			
		||||
.deleteModal {
 | 
			
		||||
  background-color: light-dark(white, #2d353c);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  border: solid 1px #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalTitle {
 | 
			
		||||
  color: #ff4646;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalContent {
 | 
			
		||||
  color: light-dark(#2d353c, white);
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalFooter {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton {
 | 
			
		||||
  background-color: #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton:hover {
 | 
			
		||||
  background-color: #ff6b6b;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,11 @@
 | 
			
		|||
import { getListMaster, getTickets, handleTicket } from '@/api/Admin'
 | 
			
		||||
import {
 | 
			
		||||
  getListMaster,
 | 
			
		||||
  getTickets,
 | 
			
		||||
  handleTicket,
 | 
			
		||||
  updateTicket,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { DataTablePagination } from '@/components/DataTable/DataTable'
 | 
			
		||||
import { create } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { create, update } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import {
 | 
			
		||||
  Badge,
 | 
			
		||||
| 
						 | 
				
			
			@ -9,12 +14,13 @@ import {
 | 
			
		|||
  HoverCard,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Select,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Text,
 | 
			
		||||
  Textarea,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { useForm } from '@mantine/form'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import { IconCheckbox, IconSquareXFilled } from '@tabler/icons-react'
 | 
			
		||||
import { IconCheckbox, IconEdit, IconSquareXFilled } from '@tabler/icons-react'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import classes from './TicketsManagement.module.css'
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +30,7 @@ type TTickets = {
 | 
			
		|||
  ticket_id: number
 | 
			
		||||
  admin_note: string
 | 
			
		||||
  action: string
 | 
			
		||||
  status: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TListTickets = {
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +85,7 @@ const TicketsManagement = () => {
 | 
			
		|||
    end_period: '',
 | 
			
		||||
    reason: '',
 | 
			
		||||
    type: '',
 | 
			
		||||
    status: '',
 | 
			
		||||
  })
 | 
			
		||||
  const [disableBtn, setDisableBtn] = useState(false)
 | 
			
		||||
  const [filter, setFilter] = useState({
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +94,7 @@ const TicketsManagement = () => {
 | 
			
		|||
  })
 | 
			
		||||
  const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
 | 
			
		||||
  const [dataReason, setDataReason] = useState<DataReason[]>([])
 | 
			
		||||
  const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const getListMasterByType = async (type: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
| 
						 | 
				
			
			@ -156,7 +165,7 @@ const TicketsManagement = () => {
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'reason',
 | 
			
		||||
      size: '15%',
 | 
			
		||||
      size: '10%',
 | 
			
		||||
      header: 'Notes',
 | 
			
		||||
      render: (row: any) => {
 | 
			
		||||
        return (
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +207,7 @@ const TicketsManagement = () => {
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'admin_note',
 | 
			
		||||
      size: '15%',
 | 
			
		||||
      size: '10%',
 | 
			
		||||
      header: 'Admin Notes',
 | 
			
		||||
      render: (row: any) => {
 | 
			
		||||
        return (
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +235,18 @@ const TicketsManagement = () => {
 | 
			
		|||
      size: '10%',
 | 
			
		||||
      header: 'Updated By',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'updated_at',
 | 
			
		||||
      size: '10%',
 | 
			
		||||
      header: 'Updated At',
 | 
			
		||||
      render: (row: any) => {
 | 
			
		||||
        if (row?.updated_at) {
 | 
			
		||||
          return (
 | 
			
		||||
            <Box>{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: '#',
 | 
			
		||||
      size: '5%',
 | 
			
		||||
| 
						 | 
				
			
			@ -254,7 +275,23 @@ const TicketsManagement = () => {
 | 
			
		|||
              height={20}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : null
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Box className={classes.optionIcon}>
 | 
			
		||||
            <IconEdit
 | 
			
		||||
              className={classes.editIcon}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setAction('update')
 | 
			
		||||
                setItem(row)
 | 
			
		||||
                form.reset()
 | 
			
		||||
                console.log(row)
 | 
			
		||||
                form.setFieldValue('status', row.status)
 | 
			
		||||
                form.setFieldValue('admin_note', row.admin_note)
 | 
			
		||||
              }}
 | 
			
		||||
              width={20}
 | 
			
		||||
              height={20}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
| 
						 | 
				
			
			@ -347,6 +384,27 @@ const TicketsManagement = () => {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleUpdate = async (values: TTickets) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await update(
 | 
			
		||||
        updateTicket + `?ticket_id=${item?.id}`,
 | 
			
		||||
        {
 | 
			
		||||
          ticket_id: item.id,
 | 
			
		||||
          admin_note: values.admin_note,
 | 
			
		||||
          status: values.status,
 | 
			
		||||
        },
 | 
			
		||||
        getAllTickets,
 | 
			
		||||
      )
 | 
			
		||||
      if (res === true) {
 | 
			
		||||
        setAction('')
 | 
			
		||||
        setIsRefuseConfirmOpen(false)
 | 
			
		||||
        form.reset()
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllTickets()
 | 
			
		||||
  }, [filter])
 | 
			
		||||
| 
						 | 
				
			
			@ -356,6 +414,7 @@ const TicketsManagement = () => {
 | 
			
		|||
      ticket_id: 0,
 | 
			
		||||
      action: '',
 | 
			
		||||
      admin_note: '',
 | 
			
		||||
      status: '',
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -416,21 +475,38 @@ const TicketsManagement = () => {
 | 
			
		|||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <Modal
 | 
			
		||||
        opened={action === 'confirm' || action === 'refuse'}
 | 
			
		||||
        opened={
 | 
			
		||||
          action === 'confirm' || action === 'refuse' || action === 'update'
 | 
			
		||||
        }
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setAction('')
 | 
			
		||||
          setIsRefuseConfirmOpen(false)
 | 
			
		||||
          form.reset()
 | 
			
		||||
        }}
 | 
			
		||||
        title={
 | 
			
		||||
          <Text pl={'sm'} fw={700} fz={'lg'}>
 | 
			
		||||
            {action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'}
 | 
			
		||||
            {action === 'confirm'
 | 
			
		||||
              ? 'Confirm Ticket'
 | 
			
		||||
              : action === 'refuse'
 | 
			
		||||
              ? 'Refuse Ticket'
 | 
			
		||||
              : 'Update Ticket'}
 | 
			
		||||
          </Text>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <form
 | 
			
		||||
          onSubmit={form.onSubmit(async (values) => {
 | 
			
		||||
            setDisableBtn(true)
 | 
			
		||||
 | 
			
		||||
            if (action === 'update') {
 | 
			
		||||
              if (values.status === 'REFUSED' && item.status !== 'REFUSED') {
 | 
			
		||||
                setIsRefuseConfirmOpen(true)
 | 
			
		||||
              } else {
 | 
			
		||||
                await handleUpdate(values)
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              await handleSave(values)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setDisableBtn(false)
 | 
			
		||||
          })}
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			@ -509,14 +585,39 @@ const TicketsManagement = () => {
 | 
			
		|||
 | 
			
		||||
            <Textarea
 | 
			
		||||
              label="Admin Notes"
 | 
			
		||||
              // required
 | 
			
		||||
              value={form.values.admin_note}
 | 
			
		||||
              onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {action === 'update' && item.status !== 'REFUSED' ? (
 | 
			
		||||
              <Switch
 | 
			
		||||
                mt="md"
 | 
			
		||||
                color="red"
 | 
			
		||||
                label={
 | 
			
		||||
                  <Text size="sm">
 | 
			
		||||
                    Change status to{' '}
 | 
			
		||||
                    <Text span color="red" fw="bold">
 | 
			
		||||
                      Refuse
 | 
			
		||||
                    </Text>
 | 
			
		||||
                  </Text>
 | 
			
		||||
                }
 | 
			
		||||
                style={{ width: 'fit-content' }}
 | 
			
		||||
                checked={form.values.status === 'REFUSED'}
 | 
			
		||||
                onChange={(event) =>
 | 
			
		||||
                  form.setFieldValue(
 | 
			
		||||
                    'status',
 | 
			
		||||
                    event.currentTarget.checked ? 'REFUSED' : 'CONFIRMED',
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              ''
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <Box ta={'center'}>
 | 
			
		||||
              <Button
 | 
			
		||||
                mt={'lg'}
 | 
			
		||||
                bg={'green'}
 | 
			
		||||
                bg={action === 'update' ? 'blue' : 'green'}
 | 
			
		||||
                type="submit"
 | 
			
		||||
                disabled={disableBtn}
 | 
			
		||||
              >
 | 
			
		||||
| 
						 | 
				
			
			@ -526,6 +627,50 @@ const TicketsManagement = () => {
 | 
			
		|||
          </Box>
 | 
			
		||||
        </form>
 | 
			
		||||
      </Modal>
 | 
			
		||||
 | 
			
		||||
      <Modal
 | 
			
		||||
        opened={isRefuseConfirmOpen}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setIsRefuseConfirmOpen(false)
 | 
			
		||||
        }}
 | 
			
		||||
        centered
 | 
			
		||||
        size="sm"
 | 
			
		||||
        classNames={{
 | 
			
		||||
          content: classes.deleteModal,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Text className={classes.deleteModalTitle}>Confirm Update</Text>
 | 
			
		||||
        <Text className={classes.deleteModalContent}>
 | 
			
		||||
          Changing ticket status to <strong>Refused</strong> will also delete
 | 
			
		||||
          all related notes.
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text className={classes.deleteModalContent}>
 | 
			
		||||
          Are you sure you want to proceed?
 | 
			
		||||
        </Text>
 | 
			
		||||
 | 
			
		||||
        <Box className={classes.deleteModalFooter}>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setIsRefuseConfirmOpen(false)
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={disableBtn}
 | 
			
		||||
          >
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            className={classes.deleteButton}
 | 
			
		||||
            onClick={async () => {
 | 
			
		||||
              setDisableBtn(true)
 | 
			
		||||
              await handleUpdate(form.values)
 | 
			
		||||
              setDisableBtn(false)
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={disableBtn}
 | 
			
		||||
          >
 | 
			
		||||
            Confirm
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,3 +60,37 @@
 | 
			
		|||
  padding-top: 5px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Thêm styles cho Modal xác nhận xóa */
 | 
			
		||||
.deleteModal {
 | 
			
		||||
  background-color: light-dark(white, #2d353c);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  border: solid 1px #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalTitle {
 | 
			
		||||
  color: #ff4646;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalContent {
 | 
			
		||||
  color: light-dark(#2d353c, white);
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalFooter {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton {
 | 
			
		||||
  background-color: #ff4646;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton:hover {
 | 
			
		||||
  background-color: #ff6b6b;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,6 +147,10 @@ const Timekeeping = () => {
 | 
			
		|||
  const [exportModalOpened, setExportModalOpened] = useState(false)
 | 
			
		||||
  const [exportOption, setExportOption] = useState('default')
 | 
			
		||||
 | 
			
		||||
  const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false)
 | 
			
		||||
  const [isDisableDeleteBtn, setIsDisableDeleteBtn] = useState(false)
 | 
			
		||||
  const [noteToDelete, setNoteToDelete] = useState<any>(null)
 | 
			
		||||
 | 
			
		||||
  const getListMasterByType = async (type: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const params = {
 | 
			
		||||
| 
						 | 
				
			
			@ -452,9 +456,9 @@ const Timekeeping = () => {
 | 
			
		|||
          month: date.month,
 | 
			
		||||
          year: date.year,
 | 
			
		||||
          working_days: workingDays,
 | 
			
		||||
          option: option
 | 
			
		||||
          option: option,
 | 
			
		||||
        },
 | 
			
		||||
        fileName
 | 
			
		||||
        fileName,
 | 
			
		||||
      )
 | 
			
		||||
      setExportModalOpened(false)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -462,6 +466,64 @@ const Timekeeping = () => {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleConfirmDelete = async () => {
 | 
			
		||||
    if (noteToDelete) {
 | 
			
		||||
      setIsDisableDeleteBtn(true)
 | 
			
		||||
      await handleDelete(noteToDelete.id)
 | 
			
		||||
      setIsDisableDeleteBtn(false)
 | 
			
		||||
      setIsDeleteConfirmOpen(false)
 | 
			
		||||
      setNoteToDelete(null)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const openDeleteConfirm = (note: any) => {
 | 
			
		||||
    setNoteToDelete(note)
 | 
			
		||||
    setIsDeleteConfirmOpen(true)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const DeleteConfirmModal = () => (
 | 
			
		||||
    <Modal
 | 
			
		||||
      opened={isDeleteConfirmOpen}
 | 
			
		||||
      onClose={() => {
 | 
			
		||||
        setIsDeleteConfirmOpen(false)
 | 
			
		||||
        setNoteToDelete(null)
 | 
			
		||||
      }}
 | 
			
		||||
      centered
 | 
			
		||||
      size="sm"
 | 
			
		||||
      classNames={{
 | 
			
		||||
        content: classes.deleteModal,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Text className={classes.deleteModalTitle}>Confirm Delete</Text>
 | 
			
		||||
      <Text className={classes.deleteModalContent}>
 | 
			
		||||
        This action will change the ticket status to <strong>Refused</strong>{' '}
 | 
			
		||||
        and delete all related notes.
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Text className={classes.deleteModalContent}>
 | 
			
		||||
        Are you sure you want to proceed?
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Box className={classes.deleteModalFooter}>
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="outline"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setIsDeleteConfirmOpen(false)
 | 
			
		||||
            setNoteToDelete(null)
 | 
			
		||||
          }}
 | 
			
		||||
          disabled={isDisableDeleteBtn}
 | 
			
		||||
        >
 | 
			
		||||
          Cancel
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          className={classes.deleteButton}
 | 
			
		||||
          onClick={handleConfirmDelete}
 | 
			
		||||
          disabled={isDisableDeleteBtn}
 | 
			
		||||
        >
 | 
			
		||||
          Delete
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
| 
						 | 
				
			
			@ -631,11 +693,7 @@ const Timekeeping = () => {
 | 
			
		|||
              >
 | 
			
		||||
                <IconTrash
 | 
			
		||||
                  className={classes.deleteIcon}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    await handleDelete(item.id)
 | 
			
		||||
                    // handleUpdateCacheMonth()
 | 
			
		||||
                    // close2()
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={() => openDeleteConfirm(item)}
 | 
			
		||||
                  width={20}
 | 
			
		||||
                  height={20}
 | 
			
		||||
                />
 | 
			
		||||
| 
						 | 
				
			
			@ -655,7 +713,7 @@ const Timekeeping = () => {
 | 
			
		|||
              data={Array.from({ length: 12 }, (_, index) => {
 | 
			
		||||
                return {
 | 
			
		||||
                  value: (1 + index).toString(),
 | 
			
		||||
                  label: (1 + index).toString()
 | 
			
		||||
                  label: (1 + index).toString(),
 | 
			
		||||
                }
 | 
			
		||||
              })}
 | 
			
		||||
              onChange={(e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -1074,20 +1132,19 @@ const Timekeeping = () => {
 | 
			
		|||
          placeholder="Choose an option"
 | 
			
		||||
          value={exportOption}
 | 
			
		||||
          onChange={(value) => setExportOption(value || 'default')}
 | 
			
		||||
          data={[
 | 
			
		||||
            { value: 'default', label: 'Default' }
 | 
			
		||||
          ]}
 | 
			
		||||
          data={[{ value: 'default', label: 'Default' }]}
 | 
			
		||||
          mb="md"
 | 
			
		||||
        />
 | 
			
		||||
        <Box style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
 | 
			
		||||
        <Box
 | 
			
		||||
          style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}
 | 
			
		||||
        >
 | 
			
		||||
          <Button variant="outline" onClick={() => setExportModalOpened(false)}>
 | 
			
		||||
            Close
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={() => handleExport(exportOption)}>
 | 
			
		||||
            Export
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button onClick={() => handleExport(exportOption)}>Export</Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Modal>
 | 
			
		||||
      <DeleteConfirmModal />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,3 +45,37 @@
 | 
			
		|||
.dialogText {
 | 
			
		||||
  color: light-dark(#2d353c, white);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Thêm styles cho Modal xác nhận xóa */
 | 
			
		||||
.deleteModal {
 | 
			
		||||
  background-color: light-dark(white, #2d353c);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  border: solid 1px rgb(9, 132, 132);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalTitle {
 | 
			
		||||
  color: rgb(9, 132, 132);
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  font-size: 1.2rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalContent {
 | 
			
		||||
  color: light-dark(#2d353c, white);
 | 
			
		||||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteModalFooter {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton {
 | 
			
		||||
  background-color: rgb(9, 132, 132);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteButton:hover {
 | 
			
		||||
  background-color: rgb(9, 132, 132);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import {
 | 
			
		|||
  Group,
 | 
			
		||||
  Modal,
 | 
			
		||||
  MultiSelect,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
| 
						 | 
				
			
			@ -24,28 +25,31 @@ const UsersManagement = () => {
 | 
			
		|||
  const [users, setUsers] = useState<TUser[]>([])
 | 
			
		||||
  const [action, setAction] = useState('')
 | 
			
		||||
  const [activeBtn, setActiveBtn] = useState(false)
 | 
			
		||||
  const [item, setItem] = useState({ id: 0 })
 | 
			
		||||
  const [item, setItem] = useState({ id: 0, is_permanent: false })
 | 
			
		||||
  const [disableBtn, setDisableBtn] = useState(false)
 | 
			
		||||
  const [info, setInfo] = useState('')
 | 
			
		||||
  const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] =
 | 
			
		||||
    useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'id',
 | 
			
		||||
      size: '3%',
 | 
			
		||||
      size: '5%',
 | 
			
		||||
      header: 'ID',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'name',
 | 
			
		||||
      size: '17%',
 | 
			
		||||
      size: '20%',
 | 
			
		||||
      header: 'Name',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'email',
 | 
			
		||||
      size: '26%',
 | 
			
		||||
      size: '25%',
 | 
			
		||||
      header: 'Email',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'permission',
 | 
			
		||||
      size: '10%',
 | 
			
		||||
      size: '20%',
 | 
			
		||||
      header: 'Permission',
 | 
			
		||||
      render: (row: TUser) => {
 | 
			
		||||
        if (row.permission.includes(',')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,9 +61,21 @@ const UsersManagement = () => {
 | 
			
		|||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'is_permanent',
 | 
			
		||||
      size: '20%',
 | 
			
		||||
      header: 'Employment Type',
 | 
			
		||||
      render: (row: TUser) => {
 | 
			
		||||
        return row.is_permanent ? (
 | 
			
		||||
          <Badge color="teal">Permanent</Badge>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Badge color="violet">Probation</Badge>
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: '#',
 | 
			
		||||
      size: '5%',
 | 
			
		||||
      size: '10%',
 | 
			
		||||
      header: 'Action',
 | 
			
		||||
      render: (row: TUser) => {
 | 
			
		||||
        return (
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +84,8 @@ const UsersManagement = () => {
 | 
			
		|||
              className={classes.editIcon}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setAction('edit')
 | 
			
		||||
                setItem(row)
 | 
			
		||||
                form.reset()
 | 
			
		||||
                form.setValues(row)
 | 
			
		||||
              }}
 | 
			
		||||
              width={20}
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +112,7 @@ const UsersManagement = () => {
 | 
			
		|||
      name: '',
 | 
			
		||||
      email: '',
 | 
			
		||||
      permission: '',
 | 
			
		||||
      is_permanent: false,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +147,7 @@ const UsersManagement = () => {
 | 
			
		|||
      const res = await update(createOrUpdateUser, values, getAll)
 | 
			
		||||
      if (res === true) {
 | 
			
		||||
        setAction('')
 | 
			
		||||
        setIsPermanentConfirmOpen(false)
 | 
			
		||||
        form.reset()
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,6 +192,7 @@ const UsersManagement = () => {
 | 
			
		|||
        opened={action === 'add' || action === 'edit'}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setAction('')
 | 
			
		||||
          setIsPermanentConfirmOpen(false)
 | 
			
		||||
          form.reset()
 | 
			
		||||
        }}
 | 
			
		||||
        title={
 | 
			
		||||
| 
						 | 
				
			
			@ -183,9 +204,15 @@ const UsersManagement = () => {
 | 
			
		|||
        <form
 | 
			
		||||
          onSubmit={form.onSubmit(async (values) => {
 | 
			
		||||
            setDisableBtn(true)
 | 
			
		||||
            action === 'edit'
 | 
			
		||||
              ? await handleUpdate(values)
 | 
			
		||||
              : await handleCreate(values)
 | 
			
		||||
            if (action === 'edit') {
 | 
			
		||||
              if (values.is_permanent && !item.is_permanent) {
 | 
			
		||||
                setIsPermanentConfirmOpen(true)
 | 
			
		||||
              } else {
 | 
			
		||||
                await handleUpdate(values)
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              await handleCreate(values)
 | 
			
		||||
            }
 | 
			
		||||
            setDisableBtn(false)
 | 
			
		||||
          })}
 | 
			
		||||
        >
 | 
			
		||||
| 
						 | 
				
			
			@ -196,6 +223,7 @@ const UsersManagement = () => {
 | 
			
		|||
              value={form.values.name}
 | 
			
		||||
              error={form.errors.name}
 | 
			
		||||
              onChange={(e) => form.setFieldValue('name', e.target.value)}
 | 
			
		||||
              required
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <TextInput
 | 
			
		||||
| 
						 | 
				
			
			@ -204,6 +232,7 @@ const UsersManagement = () => {
 | 
			
		|||
              value={form.values.email}
 | 
			
		||||
              error={form.errors.email}
 | 
			
		||||
              onChange={(e) => form.setFieldValue('email', e.target.value)}
 | 
			
		||||
              required
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <MultiSelect
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +253,25 @@ const UsersManagement = () => {
 | 
			
		|||
                  e!.filter((p) => p.trim() !== '').join(','),
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              mb={'md'}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {action === 'edit' && !item.is_permanent ? (
 | 
			
		||||
              <Switch
 | 
			
		||||
                label="Permanent employee"
 | 
			
		||||
                style={{ width: 'fit-content' }}
 | 
			
		||||
                checked={form.values.is_permanent}
 | 
			
		||||
                onChange={(event) =>
 | 
			
		||||
                  form.setFieldValue(
 | 
			
		||||
                    'is_permanent',
 | 
			
		||||
                    event.currentTarget.checked,
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              ''
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <Box ta={'center'}>
 | 
			
		||||
              {action === 'add' ? (
 | 
			
		||||
                <Button
 | 
			
		||||
| 
						 | 
				
			
			@ -273,6 +320,50 @@ const UsersManagement = () => {
 | 
			
		|||
          {info}
 | 
			
		||||
        </Code>
 | 
			
		||||
      </Modal>
 | 
			
		||||
 | 
			
		||||
      {/* Confirm change to permanent employee */}
 | 
			
		||||
      <Modal
 | 
			
		||||
        opened={isPermanentConfirmOpen}
 | 
			
		||||
        onClose={() => setIsPermanentConfirmOpen(false)}
 | 
			
		||||
        centered
 | 
			
		||||
        size="sm"
 | 
			
		||||
        classNames={{
 | 
			
		||||
          content: classes.deleteModal,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Text className={classes.deleteModalTitle}>Confirm Update</Text>
 | 
			
		||||
        <Text className={classes.deleteModalContent}>
 | 
			
		||||
          This action will change the employment type from{' '}
 | 
			
		||||
          <strong>Probation</strong> to <strong>Permanent</strong>.
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text className={classes.deleteModalContent}>
 | 
			
		||||
          Are you sure you want to proceed?
 | 
			
		||||
        </Text>
 | 
			
		||||
 | 
			
		||||
        <Box className={classes.deleteModalFooter}>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="outline"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setIsPermanentConfirmOpen(false)
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={disableBtn}
 | 
			
		||||
          >
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            className={classes.deleteButton}
 | 
			
		||||
            onClick={async () => {
 | 
			
		||||
              setDisableBtn(true)
 | 
			
		||||
              await handleUpdate(form.values)
 | 
			
		||||
              setDisableBtn(false)
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={disableBtn}
 | 
			
		||||
          >
 | 
			
		||||
            Confirm
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Modal>
 | 
			
		||||
 | 
			
		||||
      <Dialog
 | 
			
		||||
        className={classes.dialog}
 | 
			
		||||
        opened={action === 'delete'}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ export const create = async (
 | 
			
		|||
    if (res.status === false) {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Error',
 | 
			
		||||
        message: res.message ?? _NOTIFICATION_MESS.create_error,
 | 
			
		||||
        message: <div style={{ whiteSpace: 'pre-line' }}>{res.message ?? _NOTIFICATION_MESS.create_error}</div>,
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,6 +76,7 @@ export type TUser = {
 | 
			
		|||
  email: string
 | 
			
		||||
  name: string
 | 
			
		||||
  permission: string
 | 
			
		||||
  is_permanent: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type DataReason = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue