dev #130
			
				
			
		
		
		
	| 
						 | 
					@ -14,7 +14,7 @@ class CategoryController extends Controller
 | 
				
			||||||
     * @param Request $request The HTTP request object.
 | 
					     * @param Request $request The HTTP request object.
 | 
				
			||||||
     * @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
 | 
					     * @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();
 | 
					        $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);
 | 
					        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();
 | 
					        $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;
 | 
					        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,7 +35,7 @@ class LeaveManagementController extends Controller
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
            ->leftJoin("categories as reason", function ($join) {
 | 
					            ->leftJoin("categories as reason", function ($join) {
 | 
				
			||||||
                $join->on('n_reason', '=', 'reason.c_code');
 | 
					                $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(
 | 
					            ->select(
 | 
				
			||||||
                DB::raw('notes.n_user_id as n_user_id'),
 | 
					                DB::raw('notes.n_user_id as n_user_id'),
 | 
				
			||||||
| 
						 | 
					@ -44,13 +44,14 @@ class LeaveManagementController extends Controller
 | 
				
			||||||
                DB::raw('notes.n_month as month'),
 | 
					                DB::raw('notes.n_month as month'),
 | 
				
			||||||
                DB::raw('categories.c_value as leave_days'),
 | 
					                DB::raw('categories.c_value as leave_days'),
 | 
				
			||||||
                DB::raw('notes.n_day as day'),
 | 
					                DB::raw('notes.n_day as day'),
 | 
				
			||||||
 | 
					                DB::raw('notes.n_reason as reason_code'),
 | 
				
			||||||
                'reason.c_name as reason_name',
 | 
					                'reason.c_name as reason_name',
 | 
				
			||||||
                'categories.c_name as time_type_name',
 | 
					                'categories.c_name as time_type_name',
 | 
				
			||||||
                // DB::raw('SUM(categories.c_value) as leave_days')
 | 
					                // DB::raw('SUM(categories.c_value) as leave_days')
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            // ->where('notes.n_user_id', "1")
 | 
					            // ->where('notes.n_user_id', "1")
 | 
				
			||||||
            ->where('notes.n_year', $year)
 | 
					            ->where('notes.n_year', $year)
 | 
				
			||||||
            ->where('notes.n_reason', 'ONLEAVE')
 | 
					            ->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY'])
 | 
				
			||||||
            // ->groupBy("notes.n_user_id")
 | 
					            // ->groupBy("notes.n_user_id")
 | 
				
			||||||
            ->orderBy('notes.n_month')
 | 
					            ->orderBy('notes.n_month')
 | 
				
			||||||
            ->orderBy('notes.n_day')
 | 
					            ->orderBy('notes.n_day')
 | 
				
			||||||
| 
						 | 
					@ -59,7 +60,7 @@ class LeaveManagementController extends Controller
 | 
				
			||||||
                return [
 | 
					                return [
 | 
				
			||||||
                    "day" => $item->day,
 | 
					                    "day" => $item->day,
 | 
				
			||||||
                    "n_user_id" => $item->n_user_id,
 | 
					                    "n_user_id" => $item->n_user_id,
 | 
				
			||||||
                    // "time_type" => $item->time_type,
 | 
					                    "reason_code" => $item->reason_code,
 | 
				
			||||||
                    "reason_name" => $item->reason_name,
 | 
					                    "reason_name" => $item->reason_name,
 | 
				
			||||||
                    "time_type_name" => $item->time_type_name,
 | 
					                    "time_type_name" => $item->time_type_name,
 | 
				
			||||||
                    "month" => $item->month,
 | 
					                    "month" => $item->month,
 | 
				
			||||||
| 
						 | 
					@ -105,9 +106,10 @@ class LeaveManagementController extends Controller
 | 
				
			||||||
                    'leaveDay' => [
 | 
					                    'leaveDay' => [
 | 
				
			||||||
                        'id' => $item->id,
 | 
					                        'id' => $item->id,
 | 
				
			||||||
                        'ld_user_id' => $item->ld_user_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_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,
 | 
					                        'ld_note' => $item->ld_note,
 | 
				
			||||||
                        'created_at' => $item->created_at,
 | 
					                        'created_at' => $item->created_at,
 | 
				
			||||||
                        'updated_at' => $item->updated_at,
 | 
					                        'updated_at' => $item->updated_at,
 | 
				
			||||||
| 
						 | 
					@ -133,8 +135,9 @@ class LeaveManagementController extends Controller
 | 
				
			||||||
        $validatedData = $request->all();
 | 
					        $validatedData = $request->all();
 | 
				
			||||||
        $leaveDays = LeaveDays::find($validatedData['id']);
 | 
					        $leaveDays = LeaveDays::find($validatedData['id']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $leaveDays->ld_day = $validatedData['totalLeave'];
 | 
					        $leaveDays->ld_day_total = $validatedData['totalLeave'];
 | 
				
			||||||
        $leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
 | 
					        $leaveDays->ld_additional_day = $validatedData['dayAdditional'];
 | 
				
			||||||
 | 
					        $leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
 | 
				
			||||||
        $leaveDays->ld_note = $validatedData['note'];
 | 
					        $leaveDays->ld_note = $validatedData['note'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $leaveDays->save();
 | 
					        $leaveDays->save();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
 | 
				
			||||||
use App\Traits\HasOrderByRequest;
 | 
					use App\Traits\HasOrderByRequest;
 | 
				
			||||||
use App\Traits\HasSearchRequest;
 | 
					use App\Traits\HasSearchRequest;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Mail;
 | 
				
			||||||
use Illuminate\Support\Facades\Storage;
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Log;
 | 
				
			||||||
 | 
					use Modules\Admin\app\Models\Admin;
 | 
				
			||||||
use Modules\Admin\app\Models\Sprint;
 | 
					use Modules\Admin\app\Models\Sprint;
 | 
				
			||||||
use Modules\Admin\app\Models\UserCriteria;
 | 
					use Modules\Admin\app\Models\UserCriteria;
 | 
				
			||||||
 | 
					use App\Models\Files;
 | 
				
			||||||
 | 
					use App\DataTransferObjects\FileData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProfileController extends Controller
 | 
					class ProfileController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -28,6 +33,7 @@ class ProfileController extends Controller
 | 
				
			||||||
        $this->jiraService = $jiraService;
 | 
					        $this->jiraService = $jiraService;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getProfilesData(Request $request)
 | 
					    public function getProfilesData(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $user = auth('admins')->user();
 | 
					        $user = auth('admins')->user();
 | 
				
			||||||
| 
						 | 
					@ -125,7 +131,7 @@ class ProfileController extends Controller
 | 
				
			||||||
        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
					        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get all files and directories in the specified root folder
 | 
					        // Get all files and directories in the specified root folder
 | 
				
			||||||
        $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder);
 | 
					        $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return response()->json(['data' => $fileList, 'status' => true]);
 | 
					        return response()->json(['data' => $fileList, 'status' => true]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -169,7 +175,7 @@ class ProfileController extends Controller
 | 
				
			||||||
        $name = $request->input('name') ?? auth('admins')->user()->name;
 | 
					        $name = $request->input('name') ?? auth('admins')->user()->name;
 | 
				
			||||||
        // Validate the incoming files
 | 
					        // Validate the incoming files
 | 
				
			||||||
        $request->validate([
 | 
					        $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 = [];
 | 
					        $uploadedFiles = [];
 | 
				
			||||||
| 
						 | 
					@ -185,6 +191,10 @@ class ProfileController extends Controller
 | 
				
			||||||
        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
					        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
				
			||||||
            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
					            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
 | 
				
			||||||
 | 
					        $currentUser = auth('admins')->user();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($request->hasFile('files')) {
 | 
					        if ($request->hasFile('files')) {
 | 
				
			||||||
            foreach ($request->file('files') as $file) {
 | 
					            foreach ($request->file('files') as $file) {
 | 
				
			||||||
                // Store the file and get its path
 | 
					                // Store the file and get its path
 | 
				
			||||||
| 
						 | 
					@ -197,6 +207,32 @@ class ProfileController extends Controller
 | 
				
			||||||
                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
					                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                $uploadedFiles[] = $path;
 | 
					                $uploadedFiles[] = $path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Tạo URL đầy đủ cho file
 | 
				
			||||||
 | 
					                $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') 
 | 
				
			||||||
 | 
					                    ? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path)) 
 | 
				
			||||||
 | 
					                    : env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // // Gửi email thông báo cho admin
 | 
				
			||||||
 | 
					                // foreach ($adminEmails as $adminEmail) {
 | 
				
			||||||
 | 
					                //     $admin = Admin::where('email', $adminEmail)->first();
 | 
				
			||||||
 | 
					                //     if ($admin) {
 | 
				
			||||||
 | 
					                //         $this->sendFileUploadNotification(
 | 
				
			||||||
 | 
					                //             $admin,
 | 
				
			||||||
 | 
					                //             "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
 | 
				
			||||||
 | 
					                //             $fileUrl,
 | 
				
			||||||
 | 
					                //             "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
 | 
				
			||||||
 | 
					                //         );
 | 
				
			||||||
 | 
					                //     }
 | 
				
			||||||
 | 
					                // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // // Gửi email xác nhận cho người tải lên
 | 
				
			||||||
 | 
					                // $this->sendFileUploadNotification(
 | 
				
			||||||
 | 
					                //     $currentUser,
 | 
				
			||||||
 | 
					                //     "Bạn đã tải lên file {$originalFilename} thành công",
 | 
				
			||||||
 | 
					                //     $fileUrl,
 | 
				
			||||||
 | 
					                //     "[APAC Tech] {$currentUser->name} - Tải file thành công"
 | 
				
			||||||
 | 
					                // );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -237,4 +273,201 @@ class ProfileController extends Controller
 | 
				
			||||||
            'message' => 'File not found',
 | 
					            'message' => 'File not found',
 | 
				
			||||||
        ], 404);
 | 
					        ], 404);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function sendFileUploadNotification($user, $description, $url, $subject, $note)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Gửi email bất đồng bộ không cần job
 | 
				
			||||||
 | 
					            dispatch(function() use ($user, $description, $url, $subject, $note) {
 | 
				
			||||||
 | 
					                Mail::send('emails.file_upload_notification', [
 | 
				
			||||||
 | 
					                    'user' => $user,
 | 
				
			||||||
 | 
					                    'description' => $description,
 | 
				
			||||||
 | 
					                    'url' => $url, 
 | 
				
			||||||
 | 
					                    'note' => $note
 | 
				
			||||||
 | 
					                ], function ($message) use ($user, $subject) {
 | 
				
			||||||
 | 
					                    $message->to($user->email)
 | 
				
			||||||
 | 
					                        ->subject($subject);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            })->afterResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function uploadFiles(Request $request)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $request->validate([
 | 
				
			||||||
 | 
					                'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
 | 
				
			||||||
 | 
					                'name' => 'required|string|max:255', 
 | 
				
			||||||
 | 
					                'description' => 'nullable|string',
 | 
				
			||||||
 | 
					                'user_name' => 'required|string|max:255'
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $file = $request->file('file');
 | 
				
			||||||
 | 
					            $user = auth('admins')->user();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Tạo thư mục cho user nếu chưa tồn tại
 | 
				
			||||||
 | 
					            $userFolder = 'files/' . $request->user_name;
 | 
				
			||||||
 | 
					            if (!Storage::disk('public')->exists($userFolder)) {
 | 
				
			||||||
 | 
					                Storage::disk('public')->makeDirectory($userFolder);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            $path = $file->store($userFolder, 'public');
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            $fileRecord = Files::create([
 | 
				
			||||||
 | 
					                'name' => $request->name,
 | 
				
			||||||
 | 
					                'url' => $path,
 | 
				
			||||||
 | 
					                'type' => $this->getFileType($file->getClientOriginalName()),
 | 
				
			||||||
 | 
					                'description' => $request->description,
 | 
				
			||||||
 | 
					                'user_id' => Admin::where('name', $request->user_name)->first()->id
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $currentUser = Admin::where('name', $request->user_name)->first();
 | 
				
			||||||
 | 
					            // Gửi email thông báo cho người upload
 | 
				
			||||||
 | 
					            $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') 
 | 
				
			||||||
 | 
					                ? env('APP_URL') . '/image' . Storage::url($path) 
 | 
				
			||||||
 | 
					                : env('APP_URL') . Storage::url($path);
 | 
				
			||||||
 | 
					            $this->sendFileUploadNotification(
 | 
				
			||||||
 | 
					                $user,
 | 
				
			||||||
 | 
					                'Bạn đã tải lên file "' . $request->name . '" thành công',
 | 
				
			||||||
 | 
					                $fileUrl,
 | 
				
			||||||
 | 
					                "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
 | 
				
			||||||
 | 
					                $request->description ?? 'No description'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Gửi email thông báo cho tất cả admin khác
 | 
				
			||||||
 | 
					            $otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
 | 
				
			||||||
 | 
					            foreach ($otherAdmins as $admin) {
 | 
				
			||||||
 | 
					                $this->sendFileUploadNotification(
 | 
				
			||||||
 | 
					                    $admin,
 | 
				
			||||||
 | 
					                    'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
 | 
				
			||||||
 | 
					                    $fileUrl,
 | 
				
			||||||
 | 
					                    "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
 | 
				
			||||||
 | 
					                    $request->description ?? 'No description'
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return response()->json([
 | 
				
			||||||
 | 
					                'status' => true,
 | 
				
			||||||
 | 
					                'message' => 'File uploaded successfully',
 | 
				
			||||||
 | 
					                'data' => [
 | 
				
			||||||
 | 
					                    'id' => $fileRecord->id,
 | 
				
			||||||
 | 
					                    'name' => $fileRecord->name,
 | 
				
			||||||
 | 
					                    'url' => Storage::url($path),
 | 
				
			||||||
 | 
					                    'type' => $fileRecord->type,
 | 
				
			||||||
 | 
					                    'description' => $fileRecord->description
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            return response()->json([
 | 
				
			||||||
 | 
					                'status' => false,
 | 
				
			||||||
 | 
					                'message' => $e->getMessage()
 | 
				
			||||||
 | 
					            ], 500);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getFiles()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // 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';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,10 @@ use Modules\Admin\app\Models\Admin;
 | 
				
			||||||
use Modules\Admin\app\Models\Category;
 | 
					use Modules\Admin\app\Models\Category;
 | 
				
			||||||
use Modules\Admin\app\Models\Ticket;
 | 
					use Modules\Admin\app\Models\Ticket;
 | 
				
			||||||
use Modules\Admin\app\Models\Tracking;
 | 
					use Modules\Admin\app\Models\Tracking;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Log;
 | 
				
			||||||
 | 
					use App\Models\LeaveDays;
 | 
				
			||||||
 | 
					use Illuminate\Http\JsonResponse;
 | 
				
			||||||
 | 
					use App\Models\Admin as UserModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TicketController extends Controller
 | 
					class TicketController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -101,7 +105,6 @@ class TicketController extends Controller
 | 
				
			||||||
                ->paginate($request->get('per_page'))->toArray(),
 | 
					                ->paginate($request->get('per_page'))->toArray(),
 | 
				
			||||||
            ['status' => true]
 | 
					            ['status' => true]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return response()->json($responseData);
 | 
					        return response()->json($responseData);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -204,29 +207,139 @@ class TicketController extends Controller
 | 
				
			||||||
        $request->validate($rules);
 | 
					        $request->validate($rules);
 | 
				
			||||||
        // return $request;
 | 
					        // return $request;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        //Get data from request
 | 
					        // Get data from request
 | 
				
			||||||
        $startDate = $request->input('start_date'); //Start day
 | 
					        $startDate = $request->input('start_date');
 | 
				
			||||||
        $startPeriod = $request->input('start_period'); //The session begins
 | 
					        $startPeriod = $request->input('start_period');
 | 
				
			||||||
        $endDate = $request->input('end_date'); //End date
 | 
					        $endDate = $request->input('end_date');
 | 
				
			||||||
        $endPeriod = $request->input('end_period'); //Session ends
 | 
					        $endPeriod = $request->input('end_period');
 | 
				
			||||||
        $type = $request->input('type');
 | 
					        $type = $request->input('type');
 | 
				
			||||||
        $reason = $request->input('reason');
 | 
					        $reason = $request->input('reason');
 | 
				
			||||||
 | 
					        $isAccept = $request->input('is_accept') ?? false;
 | 
				
			||||||
        $user = auth('admins')->user(); // user create ticket
 | 
					        $user = auth('admins')->user(); // user create ticket
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // return $user;
 | 
					        $start_date = Carbon::create($startDate)->setTimezone(env('TIME_ZONE'));
 | 
				
			||||||
 | 
					        $end_date = Carbon::create($endDate)->setTimezone(env('TIME_ZONE'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod);
 | 
					        // --- Chỉ kiểm tra ngày phép khi loại là ONLEAVE ---
 | 
				
			||||||
        $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod);
 | 
					        if ($type === 'ONLEAVE' && !$isAccept) {
 | 
				
			||||||
        $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
 | 
					            // Get mảng ngày nghỉ 
 | 
				
			||||||
 | 
					            $dataListPeriod = $this->getAllPeriodNew($start_date, $startPeriod, $end_date, $endPeriod);
 | 
				
			||||||
 | 
					            if (empty($dataListPeriod)) {
 | 
				
			||||||
 | 
					                return AbstractController::ResultError('Không thể tính toán khoảng thời gian nghỉ hợp lệ.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Lấy thông tin tickets nghỉ phép đang ở trạng thái WAITING
 | 
				
			||||||
 | 
					            $ticketsWaiting = Ticket::where('user_id', $user->id)->where('status', 'WAITING')->where('type', 'ONLEAVE')
 | 
				
			||||||
 | 
					                ->get();
 | 
				
			||||||
 | 
					            $dataListPeriodWaiting = [];
 | 
				
			||||||
 | 
					            if ($ticketsWaiting->count() > 0) {
 | 
				
			||||||
 | 
					                foreach ($ticketsWaiting as $ticket) {
 | 
				
			||||||
 | 
					                    $dataListPeriodWaiting = array_merge($dataListPeriodWaiting, $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y');
 | 
					            // Lấy thông tin tickets nghỉ phép đang ở trạng thái CONFIRMED
 | 
				
			||||||
        $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y');
 | 
					            $ticketsConfirmed = Ticket::where('user_id', $user->id)->where('status', 'CONFIRMED')
 | 
				
			||||||
 | 
					                ->whereIn('type', ['ONLEAVE'])
 | 
				
			||||||
 | 
					                ->where(DB::raw('DATE(start_date)'), '>=', $start_date->toDateString())
 | 
				
			||||||
 | 
					                ->where(DB::raw('DATE(end_date)'), '<=', $end_date->toDateString())
 | 
				
			||||||
 | 
					                ->get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $user = auth('admins')->user();
 | 
					            $dataListPeriodConfirmed = [];
 | 
				
			||||||
 | 
					            if ($ticketsConfirmed->count() > 0) {
 | 
				
			||||||
 | 
					                foreach ($ticketsConfirmed as $ticket) {
 | 
				
			||||||
 | 
					                    $dataListPeriodConfirmed = array_merge($dataListPeriodConfirmed, $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Chuyển đổi mảng đa chiều thành mảng chuỗi để có thể so sánh
 | 
				
			||||||
 | 
					            $periodStrings = [];
 | 
				
			||||||
 | 
					            $waitingPeriodStrings = [];
 | 
				
			||||||
 | 
					            $confirmedPeriodStrings = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach ($dataListPeriod as $period) {
 | 
				
			||||||
 | 
					                if ($period['period'] == 'ALL') {
 | 
				
			||||||
 | 
					                    $periodStrings[] = $period['date'] . '_S';
 | 
				
			||||||
 | 
					                    $periodStrings[] = $period['date'] . '_C';
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $periodStrings[] = $period['date'] . '_' . $period['period'];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach ($dataListPeriodWaiting as $period) {
 | 
				
			||||||
 | 
					                if ($period['period'] == 'ALL') {
 | 
				
			||||||
 | 
					                    $waitingPeriodStrings[] = $period['date'] . '_S';
 | 
				
			||||||
 | 
					                    $waitingPeriodStrings[] = $period['date'] . '_C';
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $waitingPeriodStrings[] = $period['date'] . '_' . $period['period'];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            foreach ($dataListPeriodConfirmed as $period) {
 | 
				
			||||||
 | 
					                if ($period['period'] == 'ALL') {
 | 
				
			||||||
 | 
					                    $confirmedPeriodStrings[] = $period['date'] . '_S';
 | 
				
			||||||
 | 
					                    $confirmedPeriodStrings[] = $period['date'] . '_C';
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $confirmedPeriodStrings[] = $period['date'] . '_' . $period['period'];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra xem có sự trùng lặp giữa request mới và tickets đang chờ duyệt
 | 
				
			||||||
 | 
					            if (count(array_intersect($periodStrings, $waitingPeriodStrings)) > 0) {
 | 
				
			||||||
 | 
					                return AbstractController::ResultError('Đã có ticket đang chờ duyệt trong thời gian này, không thể tạo ticket mới!');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Kiểm tra xem có sự trùng lặp giữa request mới và tickets đã được duyệt
 | 
				
			||||||
 | 
					            if (count(array_intersect($periodStrings, $confirmedPeriodStrings)) > 0) {
 | 
				
			||||||
 | 
					                return AbstractController::ResultError('Đã có ticket được duyệt trong thời gian này, không thể tạo ticket mới!');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tạo thông báo về tickets waiting nếu có
 | 
				
			||||||
 | 
					            $waitingTicketsMessage = '';
 | 
				
			||||||
 | 
					            if (!empty($dataListPeriodWaiting)) {
 | 
				
			||||||
 | 
					                // Kiểm tra số dư ngày phép cho tickets waiting
 | 
				
			||||||
 | 
					                $waitingTicketsMessage = "Bạn đang có " . $ticketsWaiting->count() . " yêu cầu nghỉ phép chưa được duyệt";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Nếu muốn thêm chi tiết từng ticket waiting
 | 
				
			||||||
 | 
					                if ($ticketsWaiting->count() > 0) {
 | 
				
			||||||
 | 
					                    $waitingTicketsMessage .= ":\n";
 | 
				
			||||||
 | 
					                    foreach ($ticketsWaiting as $ticket) {
 | 
				
			||||||
 | 
					                        $startDateFormat = Carbon::parse($ticket->start_date)->format('d/m/Y');
 | 
				
			||||||
 | 
					                        $endDateFormat = Carbon::parse($ticket->end_date)->format('d/m/Y');
 | 
				
			||||||
 | 
					                        $waitingTicketsMessage .= "- " . $ticket->startPeriodName . " (" . $startDateFormat . ") - " .
 | 
				
			||||||
 | 
					                            $ticket->endPeriodName . " (" . $endDateFormat . ")\n";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $balanceCheckResultWaiting = $this->checkLeaveBalance($user, $dataListPeriodWaiting);
 | 
				
			||||||
 | 
					            if ($balanceCheckResultWaiting['months_info']) {
 | 
				
			||||||
 | 
					                $monthsInfoWaiting = $balanceCheckResultWaiting['months_info'];
 | 
				
			||||||
 | 
					                $waitingTicketsMessage .=  $balanceCheckResultWaiting['message'] . "\n------------------------------------------------";
 | 
				
			||||||
 | 
					                $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod, $monthsInfoWaiting);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Nếu không đủ ngày phép, trả về thông báo và không tạo ticket
 | 
				
			||||||
 | 
					            if (!$balanceCheckResult['success']) {
 | 
				
			||||||
 | 
					                $finalMessage = $waitingTicketsMessage;
 | 
				
			||||||
 | 
					                if (!empty($finalMessage)) {
 | 
				
			||||||
 | 
					                    $finalMessage .= "\n\n";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $finalMessage .= $balanceCheckResult['message'];
 | 
				
			||||||
 | 
					                $balanceCheckResult['message'] = $finalMessage . "\n\nBạn có chấp nhận không?\n";
 | 
				
			||||||
 | 
					                $balanceCheckResult['waitingTicketMessage'] = $waitingTicketsMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return AbstractController::ResultError("Không thỏa mãn điều kiện ngày phép", $balanceCheckResult);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // --- Kết thúc kiểm tra ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Nếu đủ ngày phép (hoặc loại ticket không phải ONLEAVE), tiếp tục tạo ticket
 | 
				
			||||||
        $ticket = Ticket::create([
 | 
					        $ticket = Ticket::create([
 | 
				
			||||||
            'start_date' => Carbon::create($startDate)->setTimezone(env('TIME_ZONE')),
 | 
					            'start_date' => $start_date->toDateString(),
 | 
				
			||||||
            'start_period' => $startPeriod,
 | 
					            'start_period' => $startPeriod,
 | 
				
			||||||
            'end_date' => Carbon::create($endDate)->setTimezone(env('TIME_ZONE')),
 | 
					            'end_date' => $end_date->toDateString(),
 | 
				
			||||||
            'end_period' => $endPeriod,
 | 
					            'end_period' => $endPeriod,
 | 
				
			||||||
            'type' => $type,
 | 
					            'type' => $type,
 | 
				
			||||||
            'status' => 'WAITING',
 | 
					            'status' => 'WAITING',
 | 
				
			||||||
| 
						 | 
					@ -235,24 +348,290 @@ class TicketController extends Controller
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Send notification email to admin (list)
 | 
					        // Send notification email to admin (list)
 | 
				
			||||||
 | 
					        $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod);
 | 
				
			||||||
 | 
					        $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod);
 | 
				
			||||||
 | 
					        $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y');
 | 
				
			||||||
 | 
					        $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $admins = Admin::where('permission', 'like', '%admin%')->get();
 | 
					        $admins = Admin::where('permission', 'like', '%admin%')->get();
 | 
				
			||||||
        foreach ($admins as $key => $value) {
 | 
					        foreach ($admins as $key => $value) {
 | 
				
			||||||
            $data = array(
 | 
					            $data = array(
 | 
				
			||||||
                "email_template" => "email.notification_tickets",
 | 
					                "email_template" => "email.notification_tickets",
 | 
				
			||||||
                "email" => $user->email,
 | 
					                "email" => $user->email,
 | 
				
			||||||
                "name" => $user->name,
 | 
					                "name" => $user->name,
 | 
				
			||||||
                "date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
 | 
					                "date" => optional($dataMasterStartPeriod)->c_name . " (" . $formattedStartDate . ") - " . optional($dataMasterEndPeriod)->c_name . " (" . $formattedEndDate . ")",
 | 
				
			||||||
                "type" => $dataMasterType->c_name,
 | 
					                "type" => optional($dataMasterType)->c_name,
 | 
				
			||||||
                "note" => $reason,
 | 
					                "note" => $reason,
 | 
				
			||||||
                "link" => "/tickets-management", //link đến page admin
 | 
					                "link" => "/tickets-management", //link đến page admin
 | 
				
			||||||
                "subject" => "[Ticket request] Ticket From " . $user->name
 | 
					                "subject" => "[Ticket request] Ticket From " . $user->name
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					            // Thêm kiểm tra null trước khi gửi mail
 | 
				
			||||||
 | 
					            if ($dataMasterStartPeriod && $dataMasterEndPeriod && $dataMasterType) {
 | 
				
			||||||
            Mail::to($value->email)->send(new TicketMail($data));
 | 
					            Mail::to($value->email)->send(new TicketMail($data));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Log::error("Missing category data for ticket ID: {$ticket->id}. Mail not sent.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return response()->json(['data' => $ticket, 'status' => true]);
 | 
					        return response()->json(['data' => $ticket, 'status' => true]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Kiểm tra số dư ngày phép của người dùng.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param UserModel $user Người dùng tạo ticket
 | 
				
			||||||
 | 
					     * @param array|null $dataListPeriod Danh sách các ngày xin nghỉ [['date' => 'Y-m-d', 'period' => 'ALL|S|C'], ...]
 | 
				
			||||||
 | 
					     * @return array Kết quả kiểm tra ['success' => bool, 'message' => string|null, ...]
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function checkLeaveBalance($user, ?array $dataListPeriod = null, ?array $monthsInfoWaiting = null): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Kiểm tra giới hạn nghỉ phép theo tháng
 | 
				
			||||||
 | 
					        if (!empty($dataListPeriod)) {
 | 
				
			||||||
 | 
					            return $this->checkMonthlyLeaveLimit($user, $dataListPeriod, $monthsInfoWaiting);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Đủ điều kiện
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'success' => true,
 | 
				
			||||||
 | 
					            'message' => null,
 | 
				
			||||||
 | 
					            'months_info' => []
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function getTotalAllocatedDays($user, int $year): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $leaveDaysInfo = LeaveDays::where('ld_user_id', $user->id)
 | 
				
			||||||
 | 
					            ->where('ld_year', $year)
 | 
				
			||||||
 | 
					            ->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $totalAllocated = 0;
 | 
				
			||||||
 | 
					        if ($leaveDaysInfo) {
 | 
				
			||||||
 | 
					            $totalAllocated = $leaveDaysInfo->ld_day_total + $leaveDaysInfo->ld_additional_day + $leaveDaysInfo->ld_special_leave_day;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Log::warning("No LeaveDays record found for user ID: {$user->id}, year: {$year}. Assuming 0 allocated days.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $totalAllocated;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function getUsedLeaveDays($user, int $year): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 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', $year)
 | 
				
			||||||
 | 
					            ->where('n_reason', 'ONLEAVE')
 | 
				
			||||||
 | 
					            ->sum('categories.c_value');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //Tính tổng giới hạn ngày nghỉ có phép tối đa trong tháng
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function checkMonthlyLeaveLimit($user, array $dataListPeriod, ?array $monthsInfoWaiting = null): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Danh sách ngày nghỉ theo tháng
 | 
				
			||||||
 | 
					        $requestMonths = $this->groupLeaveRequestsByMonth($dataListPeriod);
 | 
				
			||||||
 | 
					        $monthsInfo = [];
 | 
				
			||||||
 | 
					        $hasInsufficientDays = false;
 | 
				
			||||||
 | 
					        $errorMessage = '';
 | 
				
			||||||
 | 
					        $remainingDaysInMonthIsUsed = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($requestMonths as $monthKey => $monthData) {
 | 
				
			||||||
 | 
					            if ($monthsInfoWaiting) {
 | 
				
			||||||
 | 
					                foreach ($monthsInfoWaiting as $monthInfo) {
 | 
				
			||||||
 | 
					                    if ($monthInfo['month'] == $monthData['month'] && $monthInfo['year'] == $monthData['year']) {
 | 
				
			||||||
 | 
					                        $remainingDaysInMonthIsUsed += $monthInfo['remaining_days_in_month_remaining'];
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // Tính tổng số ngày nghỉ có phép trong tháng
 | 
				
			||||||
 | 
					            $usedDaysInMonth = $this->getUsedLeaveDaysInMonth($user, $monthData['year'], $monthData['month'], 'ONLEAVE');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính tổng số ngày nghỉ không phép trong tháng
 | 
				
			||||||
 | 
					            $usedDaysInMonthWithoutPay = $this->getUsedLeaveDaysInMonth($user, $monthData['year'], $monthData['month'], 'LEAVE_WITHOUT_PAY');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính tổng giới hạn ngày nghỉ có phép tối đa trong tháng
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $days_will_use = 0;
 | 
				
			||||||
 | 
					            $days_will_use_without_pay = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính tổng số ngày nghỉ trong tháng
 | 
				
			||||||
 | 
					            $totalDaysInMonth = $usedDaysInMonth + $usedDaysInMonthWithoutPay + $monthData['days_requested'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính tổng phép có trong tháng
 | 
				
			||||||
 | 
					            $totalLeaveDaysInMonth = $this->getTotalLeaveDaysInMonth($user, $monthData['year'], $monthData['month']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính tổng số ngày nghỉ có phép đến tháng hiện tại
 | 
				
			||||||
 | 
					            $totalLeaveDaysInMonthToMonth = $this->getTotalLeaveDaysInMonthToMonth($user, $monthData['year'], $monthData['month']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //Ngày phép còn lại trong tháng
 | 
				
			||||||
 | 
					            $remainingDaysInMonth = $totalLeaveDaysInMonth - $totalLeaveDaysInMonthToMonth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $remainingDaysInMonthRemaining = $remainingDaysInMonth - $remainingDaysInMonthIsUsed;
 | 
				
			||||||
 | 
					            // Xử lý các trường hợp thiếu ngày phép
 | 
				
			||||||
 | 
					            if ($remainingDaysInMonthRemaining <= 0) { //hết phép
 | 
				
			||||||
 | 
					                $hasInsufficientDays = true;
 | 
				
			||||||
 | 
					                $month_data['status'] = 'no_days_left';
 | 
				
			||||||
 | 
					                $monthMessage = "* Hiện tại bạn đã hết phép nghỉ trong tháng {$monthData['month']}/{$monthData['year']}\n - Bạn sẽ nộp: " . $monthData['days_requested'] . " ngày không phép.";
 | 
				
			||||||
 | 
					                $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $days_will_use = 0;
 | 
				
			||||||
 | 
					                $days_will_use_without_pay = $monthData['days_requested'];
 | 
				
			||||||
 | 
					            } else if ($remainingDaysInMonthRemaining < $monthData['days_requested']) { // không đủ ngày phép
 | 
				
			||||||
 | 
					                $hasInsufficientDays = true;
 | 
				
			||||||
 | 
					                $month_data['status'] = 'insufficient_days';
 | 
				
			||||||
 | 
					                $daysNotEnough = $monthData['days_requested'] - $remainingDaysInMonthRemaining;
 | 
				
			||||||
 | 
					                $monthMessage = "* Tháng {$monthData['month']}/{$monthData['year']}: \n - Số ngày phép còn lại: {$remainingDaysInMonthRemaining}, Số ngày yêu cầu: {$monthData['days_requested']}.\n - Bạn sẽ sử dụng {$remainingDaysInMonthRemaining} ngày phép và {$daysNotEnough} ngày không phép.";
 | 
				
			||||||
 | 
					                $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage;
 | 
				
			||||||
 | 
					                $remainingDaysInMonthIsUsed = $remainingDaysInMonth; // lấy số ngày phép còn lại của tháng đó
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $days_will_use = $remainingDaysInMonthRemaining;
 | 
				
			||||||
 | 
					                $days_will_use_without_pay = $daysNotEnough;
 | 
				
			||||||
 | 
					            } else if ($remainingDaysInMonthRemaining >= $monthData['days_requested']) { // Đủ ngày phép ở tháng đó
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // 1. Check thêm rule 1 tháng chỉ được nghỉ tối đa $maxDaysPerMonth ngày có phép, ngày vượt sẽ là ngày không phép
 | 
				
			||||||
 | 
					                if ($monthData['days_requested'] > $maxDaysPerMonth) {
 | 
				
			||||||
 | 
					                    $hasInsufficientDays = true;
 | 
				
			||||||
 | 
					                    $month_data['status'] = 'exceed_max_days';
 | 
				
			||||||
 | 
					                    $daysWithoutPermission = $monthData['days_requested'] - $maxDaysPerMonth;
 | 
				
			||||||
 | 
					                    $monthMessage = "* Theo quy định ngày phép tôi đa mỗi tháng là {$maxDaysPerMonth} ngày. \nTháng {$monthData['month']}/{$monthData['year']}: \n - Số ngày phép còn lại: {$remainingDaysInMonthRemaining}, Số ngày yêu cầu: {$monthData['days_requested']}.\n - Bạn sẽ sử dụng {$maxDaysPerMonth} ngày phép và {$daysWithoutPermission} ngày không phép.";
 | 
				
			||||||
 | 
					                    $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $days_will_use = $maxDaysPerMonth;
 | 
				
			||||||
 | 
					                    $days_will_use_without_pay = $daysWithoutPermission;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $days_will_use = $monthData['days_requested'];
 | 
				
			||||||
 | 
					                    $days_will_use_without_pay = 0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                $remainingDaysInMonthRemaining = $monthData['days_requested'];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $days_will_use = $monthData['days_requested'];
 | 
				
			||||||
 | 
					                $days_will_use_without_pay = 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $month_data = [
 | 
				
			||||||
 | 
					                'year' => $monthData['year'],
 | 
				
			||||||
 | 
					                'month' => $monthData['month'],
 | 
				
			||||||
 | 
					                'total_leave_days_in_month' => $totalLeaveDaysInMonth, //tổng số ngày phép
 | 
				
			||||||
 | 
					                'total_leave_days_in_month_to_month' => $totalLeaveDaysInMonthToMonth, //tổng ngày nghỉ có phép đã nghỉ
 | 
				
			||||||
 | 
					                'remaining_days_in_month' => $remainingDaysInMonth, //số ngày phép còn lại
 | 
				
			||||||
 | 
					                'days_used' => $usedDaysInMonth, //tổng số ngày nghỉ có phép đã nghỉ ở tháng hiện tại
 | 
				
			||||||
 | 
					                'days_used_without_pay' => $usedDaysInMonthWithoutPay, //tổng số ngày nghỉ không phép đã nghỉ ở tháng hiện tại
 | 
				
			||||||
 | 
					                'days_requested' => $monthData['days_requested'], //số ngày yêu cầu nghỉ của tháng
 | 
				
			||||||
 | 
					                'remaining_days_in_month_remaining' => $remainingDaysInMonthRemaining,
 | 
				
			||||||
 | 
					                'days_will_use' => $days_will_use, //Số ngày phép sẽ sử dụng
 | 
				
			||||||
 | 
					                'days_will_use_without_pay' => $days_will_use_without_pay, //Số ngày không phép sẽ sử dụng
 | 
				
			||||||
 | 
					                'status' => 'ok', // mặc định là ok
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Thêm thông tin tháng vào mảng kết quả
 | 
				
			||||||
 | 
					            $monthsInfo[] = $month_data;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Trả về kết quả tổng hợp
 | 
				
			||||||
 | 
					        if ($hasInsufficientDays) {
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'success' => false,
 | 
				
			||||||
 | 
					                'message' => $errorMessage,
 | 
				
			||||||
 | 
					                'warning_type' => 'exceed_monthly_limit',
 | 
				
			||||||
 | 
					                'months_info' => $monthsInfo
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'success' => true,
 | 
				
			||||||
 | 
					            'message' => "Đủ ngày phép cho yêu cầu.",
 | 
				
			||||||
 | 
					            'months_info' => $monthsInfo
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //Tính tổng số ngày nghỉ có phép đến tháng hiện tại
 | 
				
			||||||
 | 
					    private function getTotalLeaveDaysInMonthToMonth($user, int $year, int $month): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 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', $year)
 | 
				
			||||||
 | 
					            ->where('n_month', "<=", $month)
 | 
				
			||||||
 | 
					            ->where('n_reason', 'ONLEAVE')
 | 
				
			||||||
 | 
					            ->sum('categories.c_value');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function getTotalLeaveDaysInMonth($user, int $year, int $month): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $leaveDaysInfo = LeaveDays::where('ld_user_id', $user->id)
 | 
				
			||||||
 | 
					            ->where('ld_year', $year)
 | 
				
			||||||
 | 
					            ->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $totalAllocated = 0;
 | 
				
			||||||
 | 
					        if ($leaveDaysInfo) {
 | 
				
			||||||
 | 
					            // if ($leaveDaysInfo->ld_day_total > $month) {
 | 
				
			||||||
 | 
					            //     $totalAllocated = $month;
 | 
				
			||||||
 | 
					            // } else {
 | 
				
			||||||
 | 
					            //     $totalAllocated = $leaveDaysInfo->ld_day_total;
 | 
				
			||||||
 | 
					            // }
 | 
				
			||||||
 | 
					            $totalAllocated = $month; //(+ tạm để check)
 | 
				
			||||||
 | 
					            // bên hàm duyệt ticket sẽ check lại để + 1 ngày trước job để đảm bảo đủ ngày phép
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Log::warning("No LeaveDays record found for user ID: {$user->id}, year: {$year}. Assuming 0 allocated days.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $totalAllocated = $totalAllocated + $leaveDaysInfo->ld_additional_day + $leaveDaysInfo->ld_special_leave_day;
 | 
				
			||||||
 | 
					        return $totalAllocated;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function groupLeaveRequestsByMonth(array $dataListPeriod): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $requestMonths = [];
 | 
				
			||||||
 | 
					        foreach ($dataListPeriod as $periodData) {
 | 
				
			||||||
 | 
					            $date = Carbon::parse($periodData['date']);
 | 
				
			||||||
 | 
					            $monthKey = $date->format('Y-m'); // YYYY-MM
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!isset($requestMonths[$monthKey])) {
 | 
				
			||||||
 | 
					                $requestMonths[$monthKey] = [
 | 
				
			||||||
 | 
					                    'year' => $date->year,
 | 
				
			||||||
 | 
					                    'month' => $date->month,
 | 
				
			||||||
 | 
					                    'days_requested' => 0,
 | 
				
			||||||
 | 
					                    'days_used' => 0
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Tính số ngày yêu cầu trong tháng
 | 
				
			||||||
 | 
					            $dayValue = ($periodData['period'] === 'ALL') ? 1.0 : 0.5;
 | 
				
			||||||
 | 
					            $requestMonths[$monthKey]['days_requested'] += $dayValue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $requestMonths;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function getUsedLeaveDaysInMonth($user, int $year, int $month, string $reason): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return 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', $year)
 | 
				
			||||||
 | 
					            ->where('n_month', $month)
 | 
				
			||||||
 | 
					            ->where('n_reason', $reason)
 | 
				
			||||||
 | 
					            ->sum('categories.c_value');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function deleteTicket(Request $request)
 | 
					    public function deleteTicket(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $rules = [
 | 
					        $rules = [
 | 
				
			||||||
| 
						 | 
					@ -299,18 +678,12 @@ class TicketController extends Controller
 | 
				
			||||||
        if (!$ticket || $ticket->status !== "WAITING") {
 | 
					        if (!$ticket || $ticket->status !== "WAITING") {
 | 
				
			||||||
            return response()->json(['message' => "Ticket not found", 'status' => false]);
 | 
					            return response()->json(['message' => "Ticket not found", 'status' => false]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $results = $this->getAllPeriod($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // $admin->id != user_id of ticket ---> continue
 | 
					 | 
				
			||||||
        // Confirm
 | 
					        // Confirm
 | 
				
			||||||
        // Add records to the notes table like function Timekeeping.addNoteForUser() based on the $results array
 | 
					        // Update updated_by and admin_note in tickets table
 | 
				
			||||||
 | 
					        // Refuse
 | 
				
			||||||
        // Update updated_by and admin_note in tickets table
 | 
					        // Update updated_by and admin_note in tickets table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Send notification email to users
 | 
					        // Send notification email to users
 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Refuse
 | 
					 | 
				
			||||||
        // Update updated_by and admin_note in tickets table
 | 
					 | 
				
			||||||
        $startDate = $ticket->start_date; //Start day
 | 
					        $startDate = $ticket->start_date; //Start day
 | 
				
			||||||
        $startPeriod = $ticket->start_period; //The session begins
 | 
					        $startPeriod = $ticket->start_period; //The session begins
 | 
				
			||||||
        $endDate = $ticket->end_date; //End date
 | 
					        $endDate = $ticket->end_date; //End date
 | 
				
			||||||
| 
						 | 
					@ -321,13 +694,158 @@ class TicketController extends Controller
 | 
				
			||||||
        $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod);
 | 
					        $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod);
 | 
				
			||||||
        $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
 | 
					        $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dataMasterTypeNotes = CategoryController::getListMasterByType("REASON_NOTES");
 | 
				
			||||||
 | 
					        $onleave = null;
 | 
				
			||||||
 | 
					        $leaveWithoutPay = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($dataMasterTypeNotes) {
 | 
				
			||||||
 | 
					            // get nghỉ phép, nghỉ không phép
 | 
				
			||||||
 | 
					            $onleave = optional($dataMasterTypeNotes->where('c_code', 'ONLEAVE')->first())->c_code;
 | 
				
			||||||
 | 
					            $leaveWithoutPay = optional($dataMasterTypeNotes->where('c_code', 'LEAVE_WITHOUT_PAY')->first())->c_code;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y');
 | 
					        $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y');
 | 
				
			||||||
        $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y');
 | 
					        $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $user = Admin::find($ticket->user_id);
 | 
					        $user = Admin::find($ticket->user_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($onleave == null || $leaveWithoutPay == null) {
 | 
				
			||||||
 | 
					            return response()->json(['message' => "Data reason notes not found", 'status' => false]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($action == "confirm") {
 | 
					        if ($action == "confirm") {
 | 
				
			||||||
            foreach ($results as $result) {
 | 
					            if ($ticket->type == "ONLEAVE") {
 | 
				
			||||||
 | 
					                $dataListPeriod = $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period);
 | 
				
			||||||
 | 
					                $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod);
 | 
				
			||||||
 | 
					                // dd($balanceCheckResult,$dataListPeriod);
 | 
				
			||||||
 | 
					                if ($balanceCheckResult['success'] == false) {
 | 
				
			||||||
 | 
					                    if ($balanceCheckResult['months_info']) {
 | 
				
			||||||
 | 
					                        foreach ($balanceCheckResult['months_info'] as $monthInfo) {
 | 
				
			||||||
 | 
					                            // Lọc các ngày thuộc đúng tháng/năm này
 | 
				
			||||||
 | 
					                            $daysInMonth = array_filter($dataListPeriod, function ($item) use ($monthInfo) {
 | 
				
			||||||
 | 
					                                $date = \Carbon\Carbon::parse($item['date']);
 | 
				
			||||||
 | 
					                                return $date->year == $monthInfo['year'] && $date->month == $monthInfo['month'];
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            $daysWillUse = $monthInfo['days_will_use'] ?? 0;
 | 
				
			||||||
 | 
					                            $daysWillUseWithoutPay = $monthInfo['days_will_use_without_pay'] ?? 0;
 | 
				
			||||||
 | 
					                            // dd($daysWillUse,$daysWillUseWithoutPay,$daysInMonth);
 | 
				
			||||||
 | 
					                            foreach ($daysInMonth as $item) {
 | 
				
			||||||
 | 
					                                list($year, $month, $day) = explode('-', $item['date']);
 | 
				
			||||||
 | 
					                                $period = $item['period'];
 | 
				
			||||||
 | 
					                                $value = ($period === 'ALL') ? 1.0 : 0.5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                if ($period === 'ALL' && $daysWillUse == 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' => $ticket->user_id,
 | 
				
			||||||
 | 
					                                        'n_day' => $day,
 | 
				
			||||||
 | 
					                                        'n_month' => $month,
 | 
				
			||||||
 | 
					                                        'n_year' => $year,
 | 
				
			||||||
 | 
					                                        'n_time_type' => 'S',
 | 
				
			||||||
 | 
					                                        'n_reason' => $onleave,
 | 
				
			||||||
 | 
					                                        'n_note' => $ticket->reason
 | 
				
			||||||
 | 
					                                    ]);
 | 
				
			||||||
 | 
					                                    Notes::create([
 | 
				
			||||||
 | 
					                                        'n_user_id' => $ticket->user_id,
 | 
				
			||||||
 | 
					                                        'n_day' => $day,
 | 
				
			||||||
 | 
					                                        'n_month' => $month,
 | 
				
			||||||
 | 
					                                        'n_year' => $year,
 | 
				
			||||||
 | 
					                                        'n_time_type' => 'C',
 | 
				
			||||||
 | 
					                                        'n_reason' => $leaveWithoutPay,
 | 
				
			||||||
 | 
					                                        'n_note' => $ticket->reason
 | 
				
			||||||
 | 
					                                    ]);
 | 
				
			||||||
 | 
					                                    $daysWillUse = 0;
 | 
				
			||||||
 | 
					                                    $daysWillUseWithoutPay -= 0.5;
 | 
				
			||||||
 | 
					                                } elseif ($daysWillUse > 0) {
 | 
				
			||||||
 | 
					                                    // Dùng ngày phép trước
 | 
				
			||||||
 | 
					                                    $use = min($daysWillUse, $value);
 | 
				
			||||||
 | 
					                                    Notes::create([
 | 
				
			||||||
 | 
					                                        'n_user_id' => $ticket->user_id,
 | 
				
			||||||
 | 
					                                        'n_day' => $day,
 | 
				
			||||||
 | 
					                                        'n_month' => $month,
 | 
				
			||||||
 | 
					                                        'n_year' => $year,
 | 
				
			||||||
 | 
					                                        'n_time_type' => $period,
 | 
				
			||||||
 | 
					                                        'n_reason' => $onleave,
 | 
				
			||||||
 | 
					                                        'n_note' => $ticket->reason
 | 
				
			||||||
 | 
					                                    ]);
 | 
				
			||||||
 | 
					                                    $daysWillUse -= $use;
 | 
				
			||||||
 | 
					                                } elseif ($daysWillUseWithoutPay > 0) {
 | 
				
			||||||
 | 
					                                    // Hết phép, chuyển sang không phép
 | 
				
			||||||
 | 
					                                    $use = min($daysWillUseWithoutPay, $value);
 | 
				
			||||||
 | 
					                                    Notes::create([
 | 
				
			||||||
 | 
					                                        'n_user_id' => $ticket->user_id,
 | 
				
			||||||
 | 
					                                        'n_day' => $day,
 | 
				
			||||||
 | 
					                                        'n_month' => $month,
 | 
				
			||||||
 | 
					                                        'n_year' => $year,
 | 
				
			||||||
 | 
					                                        'n_time_type' => $period,
 | 
				
			||||||
 | 
					                                        'n_reason' => $leaveWithoutPay,
 | 
				
			||||||
 | 
					                                        'n_note' => $ticket->reason
 | 
				
			||||||
 | 
					                                    ]);
 | 
				
			||||||
 | 
					                                    $daysWillUseWithoutPay -= $use;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                // Nếu cả hai đều hết thì thôi, không tạo nữa
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    //Đủ phép
 | 
				
			||||||
 | 
					                    foreach ($dataListPeriod as $result) {
 | 
				
			||||||
 | 
					                        list($year, $month, $day) = explode('-', $result['date']);
 | 
				
			||||||
 | 
					                        Notes::create([
 | 
				
			||||||
 | 
					                            'n_user_id' => $ticket->user_id,
 | 
				
			||||||
 | 
					                            'n_day' => $day,
 | 
				
			||||||
 | 
					                            'n_month' => $month,
 | 
				
			||||||
 | 
					                            'n_year' => $year,
 | 
				
			||||||
 | 
					                            'n_time_type' => $result['period'],
 | 
				
			||||||
 | 
					                            'n_reason' => $onleave, // có phép
 | 
				
			||||||
 | 
					                            'n_note' => $ticket->reason
 | 
				
			||||||
 | 
					                        ]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $yearCheck = Carbon::parse($endDate)->year;
 | 
				
			||||||
 | 
					                // Check giá trị ld_day_total của bảng leave_days thuộc user id đó với giá trị của list item note trong bảng notes của user id đó
 | 
				
			||||||
 | 
					                $leaveDaysInfo = LeaveDays::where('ld_user_id', $ticket->user_id)
 | 
				
			||||||
 | 
					                    ->where('ld_year', $yearCheck)
 | 
				
			||||||
 | 
					                    ->first();
 | 
				
			||||||
 | 
					                if ($leaveDaysInfo) {
 | 
				
			||||||
 | 
					                    // Tính tổng số ngày nghỉ có phép đã sử dụng trong năm
 | 
				
			||||||
 | 
					                    $totalUsedLeaveDays = Notes::join('categories', function ($join) {
 | 
				
			||||||
 | 
					                        $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
				
			||||||
 | 
					                            ->where('categories.c_type', 'TIME_TYPE');
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                        ->where('n_user_id', $ticket->user_id)
 | 
				
			||||||
 | 
					                        ->where('n_year', $yearCheck)
 | 
				
			||||||
 | 
					                        ->where('n_reason', 'ONLEAVE')
 | 
				
			||||||
 | 
					                        ->sum('categories.c_value');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Tính tổng số ngày phép được cấp
 | 
				
			||||||
 | 
					                    $totalAllocatedDays = $leaveDaysInfo->ld_day_total +
 | 
				
			||||||
 | 
					                        $leaveDaysInfo->ld_additional_day +
 | 
				
			||||||
 | 
					                        $leaveDaysInfo->ld_special_leave_day;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Tính số ngày vượt quá và làm tròn lên
 | 
				
			||||||
 | 
					                    $excessDays = $totalUsedLeaveDays - $totalAllocatedDays;
 | 
				
			||||||
 | 
					                    $roundedExcessDays = ceil($excessDays); // Làm tròn lên số nguyên gần nhất
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Kiểm tra nếu số ngày đã sử dụng vượt quá số ngày được cấp
 | 
				
			||||||
 | 
					                    if ($roundedExcessDays > 0) {
 | 
				
			||||||
 | 
					                        Log::warning("User ID: {$ticket->user_id} has used more leave days ({$totalUsedLeaveDays}) than allocated ({$totalAllocatedDays})");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Cập nhật cột ld_day_total với số ngày đã làm tròn
 | 
				
			||||||
 | 
					                        if ($roundedExcessDays > 0) {
 | 
				
			||||||
 | 
					                            $leaveDaysInfo->ld_day_total += $roundedExcessDays;
 | 
				
			||||||
 | 
					                            $leaveDaysInfo->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Log::info("Updated leave days for User ID: {$ticket->user_id}. Added {$roundedExcessDays} days (rounded from {$excessDays})");
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if ($ticket->type == "WFH") {
 | 
				
			||||||
 | 
					                $dataListPeriod = $this->getAllPeriod($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period);
 | 
				
			||||||
 | 
					                foreach ($dataListPeriod as $result) {
 | 
				
			||||||
                list($year, $month, $day) = explode('-', $result['date']);
 | 
					                list($year, $month, $day) = explode('-', $result['date']);
 | 
				
			||||||
                Notes::create([
 | 
					                Notes::create([
 | 
				
			||||||
                    'n_user_id' => $ticket->user_id,
 | 
					                    'n_user_id' => $ticket->user_id,
 | 
				
			||||||
| 
						 | 
					@ -339,7 +857,8 @@ class TicketController extends Controller
 | 
				
			||||||
                    'n_note' => $ticket->reason
 | 
					                    'n_note' => $ticket->reason
 | 
				
			||||||
                ]);
 | 
					                ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if ($ticket->type == "WFH") {
 | 
					
 | 
				
			||||||
 | 
					                    //WFH - start tracking
 | 
				
			||||||
                    $type =  $result['period'];
 | 
					                    $type =  $result['period'];
 | 
				
			||||||
                    $date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
 | 
					                    $date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -369,6 +888,7 @@ class TicketController extends Controller
 | 
				
			||||||
                            'created_at' => $end->setTimezone('UTC')
 | 
					                            'created_at' => $end->setTimezone('UTC')
 | 
				
			||||||
                        ]
 | 
					                        ]
 | 
				
			||||||
                    ]);
 | 
					                    ]);
 | 
				
			||||||
 | 
					                    //WFH - end tracking
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -424,6 +944,112 @@ class TicketController extends Controller
 | 
				
			||||||
        return response()->json(['message' => "failed", 'status' => false]);
 | 
					        return response()->json(['message' => "failed", 'status' => false]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function getAllPeriodNew($startDate, $startPeriod, $endDate, $endPeriod)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Đảm bảo $startDate và $endDate là đối tượng Carbon
 | 
				
			||||||
 | 
					        if (!($startDate instanceof Carbon)) {
 | 
				
			||||||
 | 
					            $startDate = Carbon::parse($startDate);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!($endDate instanceof Carbon)) {
 | 
				
			||||||
 | 
					            $endDate = Carbon::parse($endDate);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Create an array to contain the results
 | 
				
			||||||
 | 
					        $results = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use CarbonPeriod to create a period from the start date to the end date
 | 
				
			||||||
 | 
					        $period = CarbonPeriod::create($startDate, $endDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $time_type = Category::where('c_type', 'TIME_TYPE')->get()->keyBy('c_code');
 | 
				
			||||||
 | 
					        $morning = $time_type->get('S');
 | 
				
			||||||
 | 
					        $afternoon = $time_type->get('C');
 | 
				
			||||||
 | 
					        $all_day = $time_type->get('ALL');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get all Saturday work schedules and sort them by date in descending order
 | 
				
			||||||
 | 
					        $saturday_work_schedules = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')
 | 
				
			||||||
 | 
					            ->get()
 | 
				
			||||||
 | 
					            ->sortByDesc(function ($item) {
 | 
				
			||||||
 | 
					                // Parse the date string from c_code to a Carbon instance for proper comparison
 | 
				
			||||||
 | 
					                return Carbon::createFromFormat('d-m-Y', $item->c_code);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        // get day work special
 | 
				
			||||||
 | 
					        $day_work_special = Category::where('c_type', 'DAY_WORK_SPECIAL')
 | 
				
			||||||
 | 
					            ->get()
 | 
				
			||||||
 | 
					            ->sortByDesc(function ($item) {
 | 
				
			||||||
 | 
					                return Carbon::createFromFormat('d-m-Y', $item->c_code);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get the most recent schedule date (first item after sorting)
 | 
				
			||||||
 | 
					        $latest_schedule = $saturday_work_schedules->first();
 | 
				
			||||||
 | 
					        $latestScheduleDate = Carbon::createFromFormat('d-m-Y', $latest_schedule->c_code);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!$morning || !$afternoon || !$all_day) {
 | 
				
			||||||
 | 
					            // Handle error: TIME_TYPE categories not found
 | 
				
			||||||
 | 
					            Log::error("TIME_TYPE categories (S, C, ALL) not found in database.");
 | 
				
			||||||
 | 
					            return []; // Return empty or throw exception
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $special_dates = [];
 | 
				
			||||||
 | 
					        foreach ($day_work_special as $item) {
 | 
				
			||||||
 | 
					            $special_dates[] = Carbon::createFromFormat('d-m-Y', $item->c_code)->toDateString();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        foreach ($period as $date) {
 | 
				
			||||||
 | 
					            // Check phải ngày thứ 7 đặc biệt thì tính như ngày bình thường
 | 
				
			||||||
 | 
					            if (in_array($date->toDateString(), $special_dates)) {
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Check if the current day is a Saturday
 | 
				
			||||||
 | 
					                if ($date->dayOfWeek === Carbon::SATURDAY) {
 | 
				
			||||||
 | 
					                    if ($latest_schedule) {
 | 
				
			||||||
 | 
					                        $weeksDifference = $latestScheduleDate->startOfDay()->diffInWeeks($date->copy()->startOfDay());
 | 
				
			||||||
 | 
					                        $isSaturdayWorkDay = ($weeksDifference % 2 === 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // echo $date->toDateString() . ' - ' . ($isSaturdayWorkDay ? 'Làm việc' : 'Nghỉ') . "<br>";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if ($isSaturdayWorkDay) {
 | 
				
			||||||
 | 
					                        $results[] = ['date' => $date->toDateString(), 'period' => "S"];
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                // Skip Sundays entirely
 | 
				
			||||||
 | 
					                else if ($date->dayOfWeek === Carbon::SUNDAY) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($date->isSameDay($startDate)) {
 | 
				
			||||||
 | 
					                //If the start date is morning, add afternoon
 | 
				
			||||||
 | 
					                if ($startDate->isSameDay($endDate)) { // Nghỉ trong cùng 1 ngày
 | 
				
			||||||
 | 
					                    if ($startPeriod == $endPeriod) { // Cùng 1 buổi (S hoặc C)
 | 
				
			||||||
 | 
					                        $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod];
 | 
				
			||||||
 | 
					                    } else { // Khác buổi (S đến C) -> cả ngày
 | 
				
			||||||
 | 
					                        $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code];
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else { // Ngày bắt đầu khác ngày kết thúc
 | 
				
			||||||
 | 
					                    if ($startPeriod == $morning->c_code) { // Bắt đầu từ sáng -> tính cả ngày
 | 
				
			||||||
 | 
					                        $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code];
 | 
				
			||||||
 | 
					                    } else { // Bắt đầu từ chiều -> tính buổi chiều
 | 
				
			||||||
 | 
					                        $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod]; // Là $afternoon->c_code
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } elseif ($date->isSameDay($endDate)) { // Ngày kết thúc (khác ngày bắt đầu)
 | 
				
			||||||
 | 
					                if ($endPeriod == $afternoon->c_code) { // Kết thúc vào buổi chiều -> tính cả ngày
 | 
				
			||||||
 | 
					                    $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code];
 | 
				
			||||||
 | 
					                } else { // Kết thúc vào buổi sáng -> tính buổi sáng
 | 
				
			||||||
 | 
					                    $results[] = ['date' => $date->toDateString(), 'period' => $endPeriod]; // Là $morning->c_code
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else { // Những ngày ở giữa
 | 
				
			||||||
 | 
					                $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Returns results
 | 
				
			||||||
 | 
					        return $results;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function getAllPeriod($startDate, $startPeriod, $endDate, $endPeriod)
 | 
					    private function getAllPeriod($startDate, $startPeriod, $endDate, $endPeriod)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        //Create an array to contain the results
 | 
					        //Create an array to contain the results
 | 
				
			||||||
| 
						 | 
					@ -488,4 +1114,33 @@ class TicketController extends Controller
 | 
				
			||||||
        //Returns results
 | 
					        //Returns results
 | 
				
			||||||
        return $results;
 | 
					        return $results;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Tính tổng số ngày nghỉ từ mảng các khoảng thời gian.
 | 
				
			||||||
 | 
					     * 'ALL' = 1 ngày, 'S'/'C' = 0.5 ngày.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param array $dataListPeriod Mảng các khoảng thời gian nghỉ [['date' => 'Y-m-d', 'period' => 'ALL|S|C'], ...]
 | 
				
			||||||
 | 
					     * @return float Tổng số ngày nghỉ
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private function calculateTotalLeaveDays(array $dataListPeriod): float
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $totalDays = 0.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($dataListPeriod as $periodData) {
 | 
				
			||||||
 | 
					            if (isset($periodData['period'])) {
 | 
				
			||||||
 | 
					                switch ($periodData['period']) {
 | 
				
			||||||
 | 
					                    case 'ALL':
 | 
				
			||||||
 | 
					                        $totalDays += 1.0;
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    case 'S': // Buổi sáng
 | 
				
			||||||
 | 
					                    case 'C': // Buổi chiều
 | 
				
			||||||
 | 
					                        $totalDays += 0.5;
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                        // Có thể thêm default case để xử lý lỗi nếu cần
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $totalDays;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -152,8 +152,6 @@ class TimekeepingController extends Controller
 | 
				
			||||||
        return response()->json(['status' => true, 'message' => 'Add successfully']);
 | 
					        return response()->json(['status' => true, 'message' => 'Add successfully']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function updateCacheMonth(Request $request)
 | 
					    public function updateCacheMonth(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $month = $request->month;
 | 
					        $month = $request->month;
 | 
				
			||||||
| 
						 | 
					@ -180,6 +178,71 @@ class TimekeepingController extends Controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $note = Notes::find($id);
 | 
					        $note = Notes::find($id);
 | 
				
			||||||
        if ($note) {
 | 
					        if ($note) {
 | 
				
			||||||
 | 
					            $n_month = $note->n_month;
 | 
				
			||||||
 | 
					            $n_year = $note->n_year;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($note->n_reason == "ONLEAVE") {
 | 
				
			||||||
 | 
					                // Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id
 | 
				
			||||||
 | 
					                // $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id
 | 
				
			||||||
 | 
					                $leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (count($leaveWithoutPay) > 0) {
 | 
				
			||||||
 | 
					                    $deletedValue = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
 | 
				
			||||||
 | 
					                    $needUpdate = $deletedValue;
 | 
				
			||||||
 | 
					                    // dd($needUpdate, $leaveWithoutPay);
 | 
				
			||||||
 | 
					                    foreach ($leaveWithoutPay as $lwNote) {
 | 
				
			||||||
 | 
					                        if ($needUpdate <= 0) break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if ($lwNote->n_time_type === 'ALL') {
 | 
				
			||||||
 | 
					                            if ($needUpdate == 1.0) {
 | 
				
			||||||
 | 
					                                // Chuyển cả note ALL thành phép
 | 
				
			||||||
 | 
					                                $lwNote->update(['n_reason' => 'ONLEAVE']);
 | 
				
			||||||
 | 
					                                $needUpdate = 0;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            } else { // $needUpdate == 0.5
 | 
				
			||||||
 | 
					                                // Tách ALL thành 2 note S và C, chuyển S thành phép, C giữ không phép
 | 
				
			||||||
 | 
					                                Notes::create([
 | 
				
			||||||
 | 
					                                    'n_user_id' => $lwNote->n_user_id,
 | 
				
			||||||
 | 
					                                    'n_day' => $lwNote->n_day,
 | 
				
			||||||
 | 
					                                    'n_month' => $lwNote->n_month,
 | 
				
			||||||
 | 
					                                    'n_year' => $lwNote->n_year,
 | 
				
			||||||
 | 
					                                    'n_time_type' => 'S',
 | 
				
			||||||
 | 
					                                    'n_reason' => 'ONLEAVE',
 | 
				
			||||||
 | 
					                                    'n_note' => $lwNote->n_note
 | 
				
			||||||
 | 
					                                ]);
 | 
				
			||||||
 | 
					                                Notes::create([
 | 
				
			||||||
 | 
					                                    'n_user_id' => $lwNote->n_user_id,
 | 
				
			||||||
 | 
					                                    'n_day' => $lwNote->n_day,
 | 
				
			||||||
 | 
					                                    'n_month' => $lwNote->n_month,
 | 
				
			||||||
 | 
					                                    'n_year' => $lwNote->n_year,
 | 
				
			||||||
 | 
					                                    'n_time_type' => 'C',
 | 
				
			||||||
 | 
					                                    'n_reason' => 'LEAVE_WITHOUT_PAY',
 | 
				
			||||||
 | 
					                                    'n_note' => $lwNote->n_note
 | 
				
			||||||
 | 
					                                ]);
 | 
				
			||||||
 | 
					                                $lwNote->delete();
 | 
				
			||||||
 | 
					                                $needUpdate = 0;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            // Nếu $lwNote->n_time_type == 'S' hoặc 'C' => 0.5
 | 
				
			||||||
 | 
					                            if ($needUpdate == 1.0) {
 | 
				
			||||||
 | 
					                                // Chuyển cả note ALL thành phép
 | 
				
			||||||
 | 
					                                $lwNote->update(['n_reason' => 'ONLEAVE']);
 | 
				
			||||||
 | 
					                                $needUpdate -= 0.5;
 | 
				
			||||||
 | 
					                            } else { // $needUpdate == 0.5
 | 
				
			||||||
 | 
					                                // S hoặc C, chỉ cần chuyển đúng 0.5 ngày
 | 
				
			||||||
 | 
					                                $lwNote->update(['n_reason' => 'ONLEAVE']);
 | 
				
			||||||
 | 
					                                $needUpdate = 0;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Khi note phép và k tồn tại nghỉ không phép => phép + dồn cho tháng sau
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            $note->delete();
 | 
					            $note->delete();
 | 
				
			||||||
            $this->createOrUpdateRecordForCurrentMonth($month, $year);
 | 
					            $this->createOrUpdateRecordForCurrentMonth($month, $year);
 | 
				
			||||||
            return response()->json(['message' => 'Delete success', 'status' => true]);
 | 
					            return response()->json(['message' => 'Delete success', 'status' => true]);
 | 
				
			||||||
| 
						 | 
					@ -206,7 +269,7 @@ class TimekeepingController extends Controller
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Lọc chỉ lấy user có permission bao gồm staff
 | 
					        // Lọc chỉ lấy user có permission bao gồm staff
 | 
				
			||||||
        $staffData = array_filter($responseData['data'], function($user) {
 | 
					        $staffData = array_filter($responseData['data'], function ($user) {
 | 
				
			||||||
            return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
 | 
					            return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,6 +174,9 @@ Route::middleware('api')
 | 
				
			||||||
                Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
					                Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
                Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
					                Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
                Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
					                Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
 | 
					                Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
 | 
					                Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
 | 
					                Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Route::group([
 | 
					            Route::group([
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,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
 | 
					class InitializeLeaveDaysCommand extends Command
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    protected $signature = 'initialize:leavedays {year?}';
 | 
					    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()
 | 
					    public function __construct()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ class InitializeLeaveDaysCommand extends Command
 | 
				
			||||||
    public function handle()
 | 
					    public function handle()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $year = $this->argument('year');
 | 
					        $year = $this->argument('year');
 | 
				
			||||||
        InitializeLeaveDays::dispatch($year);
 | 
					        // Không sử dụng nữa, theo rule mới
 | 
				
			||||||
 | 
					        // InitializeLeaveDays::dispatch($year);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@
 | 
				
			||||||
namespace App\Console;
 | 
					namespace App\Console;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use App\Jobs\DeductLeaveDays;
 | 
					use App\Jobs\DeductLeaveDays;
 | 
				
			||||||
 | 
					use App\Jobs\AddMonthlyLeaveDays;
 | 
				
			||||||
use Illuminate\Console\Scheduling\Schedule;
 | 
					use Illuminate\Console\Scheduling\Schedule;
 | 
				
			||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 | 
					use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +25,7 @@ class Kernel extends ConsoleKernel
 | 
				
			||||||
        // ->dailyAt('18:00');
 | 
					        // ->dailyAt('18:00');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
 | 
					        // Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
 | 
				
			||||||
        $schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
 | 
					        // $schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
 | 
				
			||||||
        $schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
 | 
					        $schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Chạy buổi sáng lúc 12:00
 | 
					        // Chạy buổi sáng lúc 12:00
 | 
				
			||||||
| 
						 | 
					@ -32,6 +33,9 @@ class Kernel extends ConsoleKernel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Chạy buổi chiều lúc 17:30
 | 
					        // Chạy buổi chiều lúc 17:30
 | 
				
			||||||
        $schedule->command('attendance:check C')->dailyAt('17:30');
 | 
					        $schedule->command('attendance:check C')->dailyAt('17:30');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Chạy vào 00:01 ngày đầu tiên của mỗi tháng
 | 
				
			||||||
 | 
					        $schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,7 +41,7 @@ class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, With
 | 
				
			||||||
        $stt = 0;
 | 
					        $stt = 0;
 | 
				
			||||||
        foreach ($this->data as $index => $user) {
 | 
					        foreach ($this->data as $index => $user) {
 | 
				
			||||||
            $totalDayOff = 0;
 | 
					            $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
 | 
					            // Tính tổng ngày nghỉ theo tháng
 | 
				
			||||||
            $monthlyLeaves = array_fill(1, 12, 0);
 | 
					            $monthlyLeaves = array_fill(1, 12, 0);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,69 @@
 | 
				
			||||||
 | 
					<?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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddMonthlyLeaveDays 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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public function handle(): void
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    $users = User::get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    foreach ($users as $user) {
 | 
				
			||||||
 | 
					      $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 {
 | 
				
			||||||
 | 
					        // Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại
 | 
				
			||||||
 | 
					        if ($leaveDay->ld_day_total < $this->month) {
 | 
				
			||||||
 | 
					          // Cập nhật số ngày phép bằng với tháng hiện tại
 | 
				
			||||||
 | 
					          $oldDays = $leaveDay->ld_day_total;
 | 
				
			||||||
 | 
					          $leaveDay->ld_day_total = $this->month;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Xử lý ghi chú
 | 
				
			||||||
 | 
					          $newNote = "Cập nhật ngày phép đến tháng " . $this->month;
 | 
				
			||||||
 | 
					          if (!empty($leaveDay->ld_note)) {
 | 
				
			||||||
 | 
					            // Nếu đã có ghi chú, thêm ghi chú mới vào và xuống dòng
 | 
				
			||||||
 | 
					            $leaveDay->ld_note = $leaveDay->ld_note . "\n" . $newNote;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // Nếu chưa có ghi chú, gán ghi chú mới
 | 
				
			||||||
 | 
					            $leaveDay->ld_note = $newNote;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          $leaveDay->save();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ class DeductLeaveDays implements ShouldQueue
 | 
				
			||||||
        foreach ($users as $user) {
 | 
					        foreach ($users as $user) {
 | 
				
			||||||
            $existingData = LeaveDays::where('ld_user_id', $user->id)
 | 
					            $existingData = LeaveDays::where('ld_user_id', $user->id)
 | 
				
			||||||
                ->where('ld_year', $this->year)
 | 
					                ->where('ld_year', $this->year)
 | 
				
			||||||
                ->where('ld_date_additional', ">", 0)
 | 
					                ->where('ld_additional_day', ">", 0)
 | 
				
			||||||
                ->first();
 | 
					                ->first();
 | 
				
			||||||
            if (!$existingData) {
 | 
					            if (!$existingData) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
| 
						 | 
					@ -59,11 +59,11 @@ class DeductLeaveDays implements ShouldQueue
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
            if ($totalLeaveDaysByMonth) {
 | 
					            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)
 | 
					                //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) {
 | 
					                if ($existingData->ld_additional_day > $totalLeaveDaysByMonth->leave_days) {
 | 
				
			||||||
                    LeaveDays::where('ld_year', $this->year)
 | 
					                    LeaveDays::where('ld_year', $this->year)
 | 
				
			||||||
                        ->where('ld_user_id', $user->id)
 | 
					                        ->where('ld_user_id', $user->id)
 | 
				
			||||||
                        ->update([
 | 
					                        ->update([
 | 
				
			||||||
                            'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
 | 
					                            'ld_additional_day' => $totalLeaveDaysByMonth->leave_days,
 | 
				
			||||||
                        ]);
 | 
					                        ]);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,7 @@ class DeductLeaveDays implements ShouldQueue
 | 
				
			||||||
                LeaveDays::where('ld_year', $this->year)
 | 
					                LeaveDays::where('ld_year', $this->year)
 | 
				
			||||||
                    ->where('ld_user_id', $user->id)
 | 
					                    ->where('ld_user_id', $user->id)
 | 
				
			||||||
                    ->update([
 | 
					                    ->update([
 | 
				
			||||||
                        'ld_date_additional' => "0",
 | 
					                        'ld_additional_day' => "0",
 | 
				
			||||||
                    ]);
 | 
					                    ]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ class InitializeLeaveDays implements ShouldQueue
 | 
				
			||||||
    public function handle(): void
 | 
					    public function handle(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $users = User::get();
 | 
					        $users = User::get();
 | 
				
			||||||
        $ld_day = 12;
 | 
					        $ld_day_total = 12;
 | 
				
			||||||
        foreach ($users as $user) {
 | 
					        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
 | 
					            // 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)
 | 
					            $existingData = LeaveDays::where('ld_user_id', $user->id)
 | 
				
			||||||
| 
						 | 
					@ -51,11 +51,11 @@ class InitializeLeaveDays implements ShouldQueue
 | 
				
			||||||
                ->where('ld_year', $this->year - 1)
 | 
					                ->where('ld_year', $this->year - 1)
 | 
				
			||||||
                ->first();
 | 
					                ->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $ld_date_additional = 0;
 | 
					            $ld_additional_day = 0;
 | 
				
			||||||
            $ld_note = '';
 | 
					            $ld_note = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($previousYearData) {
 | 
					            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) {
 | 
					                $totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
 | 
				
			||||||
                    $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
					                    $join->on('notes.n_time_type', '=', 'categories.c_code')
 | 
				
			||||||
                        ->where('categories.c_type', 'TIME_TYPE');
 | 
					                        ->where('categories.c_type', 'TIME_TYPE');
 | 
				
			||||||
| 
						 | 
					@ -71,9 +71,9 @@ class InitializeLeaveDays implements ShouldQueue
 | 
				
			||||||
                    ->groupBy(DB::raw('notes.n_year'))
 | 
					                    ->groupBy(DB::raw('notes.n_year'))
 | 
				
			||||||
                    ->first();
 | 
					                    ->first();
 | 
				
			||||||
                if ($totalLeaveDaysByMonth) {
 | 
					                if ($totalLeaveDaysByMonth) {
 | 
				
			||||||
                    $ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
 | 
					                    $ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
 | 
				
			||||||
                    if ($ld_date_additional < 0) {
 | 
					                    if ($ld_additional_day < 0) {
 | 
				
			||||||
                        $ld_date_additional = 0;
 | 
					                        $ld_additional_day = 0;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                $ld_note = 'Cộng dồn ngày phép năm cũ';
 | 
					                $ld_note = 'Cộng dồn ngày phép năm cũ';
 | 
				
			||||||
| 
						 | 
					@ -82,9 +82,9 @@ class InitializeLeaveDays implements ShouldQueue
 | 
				
			||||||
            // Tạo dữ liệu cho năm hiện tại
 | 
					            // Tạo dữ liệu cho năm hiện tại
 | 
				
			||||||
            LeaveDays::insert([
 | 
					            LeaveDays::insert([
 | 
				
			||||||
                'ld_user_id' => $user->id,
 | 
					                'ld_user_id' => $user->id,
 | 
				
			||||||
                'ld_day' => $ld_day,
 | 
					                'ld_day_total' => $ld_day_total,
 | 
				
			||||||
                'ld_year' => $this->year,
 | 
					                'ld_year' => $this->year,
 | 
				
			||||||
                'ld_date_additional' => $ld_date_additional,
 | 
					                'ld_additional_day' => $ld_additional_day,
 | 
				
			||||||
                'ld_note' => $ld_note,
 | 
					                'ld_note' => $ld_note,
 | 
				
			||||||
                'created_at' => now(),
 | 
					                'created_at' => now(),
 | 
				
			||||||
                'updated_at' => now(),
 | 
					                'updated_at' => now(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
					    use HasFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $fillable = [
 | 
					    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';
 | 
					    protected $table = 'leave_days';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,13 @@ class Notes extends Model
 | 
				
			||||||
    use HasFactory;
 | 
					    use HasFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $fillable = [
 | 
					    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',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -25,7 +31,7 @@ class Notes extends Model
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return self::leftJoin("categories as reason", function ($join) {
 | 
					        return self::leftJoin("categories as reason", function ($join) {
 | 
				
			||||||
            $join->on('n_reason', '=', 'reason.c_code');
 | 
					            $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) {
 | 
					            ->leftJoin("categories as timeTypes", function ($join) {
 | 
				
			||||||
                $join->on('n_time_type', '=', 'timeTypes.c_code');
 | 
					                $join->on('n_time_type', '=', 'timeTypes.c_code');
 | 
				
			||||||
| 
						 | 
					@ -47,4 +53,18 @@ class Notes extends Model
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            ->get();
 | 
					            ->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();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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_total', 'ld_day_total');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::table('leave_days', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->renameColumn('ld_day_total', 'ld_day_total');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
 | 
				
			||||||
        //     'name' => 'Test User',
 | 
					        //     'name' => 'Test User',
 | 
				
			||||||
        //     'email' => 'test@example.com',
 | 
					        //     'email' => 'test@example.com',
 | 
				
			||||||
        // ]);
 | 
					        // ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->call([
 | 
				
			||||||
 | 
					            UserSeeder::class,
 | 
				
			||||||
 | 
					            FileSeeder::class,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <title>Thông báo tải lên file mới</title>
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        body {
 | 
				
			||||||
 | 
					            font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					            line-height: 1.6;
 | 
				
			||||||
 | 
					            color: #333;
 | 
				
			||||||
 | 
					            margin: 0;
 | 
				
			||||||
 | 
					            padding: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .container {
 | 
				
			||||||
 | 
					            max-width: 600px;
 | 
				
			||||||
 | 
					            margin: 0 auto;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .header {
 | 
				
			||||||
 | 
					            background-color: #1a73e8;
 | 
				
			||||||
 | 
					            color: white;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .content {
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            background-color: #f9f9f9;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .button {
 | 
				
			||||||
 | 
					            display: inline-block;
 | 
				
			||||||
 | 
					            padding: 12px 24px;
 | 
				
			||||||
 | 
					            background-color: #1a73e8;
 | 
				
			||||||
 | 
					            color: white;
 | 
				
			||||||
 | 
					            text-decoration: none;
 | 
				
			||||||
 | 
					            border-radius: 4px;
 | 
				
			||||||
 | 
					            margin: 20px 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .footer {
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            background-color: #f5f5f5;
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            color: #666;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <div class="container">
 | 
				
			||||||
 | 
					        <div class="header">
 | 
				
			||||||
 | 
					            <h1>Thông báo tải lên file mới</h1>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="content">
 | 
				
			||||||
 | 
					            <p>Xin chào {{ $user->name }},</p>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <p>{{ $description }} ở hệ thống APAC Tech.</p>
 | 
				
			||||||
 | 
					            <p>Note: {{ $note }}</p>
 | 
				
			||||||
 | 
					            <p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div style="text-align: center;">
 | 
				
			||||||
 | 
					                <a href="{{ $url }}" class="button">Kiểm tra ngay</a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <p>Trân trọng,<br>Đội ngũ APAC Tech</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="footer">
 | 
				
			||||||
 | 
					            <p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html> 
 | 
				
			||||||
| 
						 | 
					@ -119,3 +119,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Download File
 | 
					// Download File
 | 
				
			||||||
export const downloadFile = API_URL + 'v1/admin/download-file'
 | 
					export const downloadFile = API_URL + 'v1/admin/download-file'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Files APIs
 | 
				
			||||||
 | 
					export const getFiles = API_URL + 'v1/admin/profile/files'
 | 
				
			||||||
 | 
					export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
 | 
				
			||||||
 | 
					export const deleteFileById = API_URL + 'v1/admin/profile/files'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,498 +1,330 @@
 | 
				
			||||||
import {
 | 
					import { getFiles, uploadFiles } from '@/api/Admin'
 | 
				
			||||||
  deleteFile,
 | 
					 | 
				
			||||||
  getAllFilesInProfiles,
 | 
					 | 
				
			||||||
  updateProfileFolder,
 | 
					 | 
				
			||||||
} from '@/api/Admin'
 | 
					 | 
				
			||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
					 | 
				
			||||||
import { get } from '@/rtk/helpers/apiService'
 | 
					import { get } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
					import { getAccessToken } from '@/rtk/localStorage'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Box,
 | 
					  Box,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  FileInput,
 | 
					  Card,
 | 
				
			||||||
 | 
					  Collapse,
 | 
				
			||||||
  Group,
 | 
					  Group,
 | 
				
			||||||
  Modal,
 | 
					  Modal,
 | 
				
			||||||
  RenderTreeNodePayload,
 | 
					 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Tooltip,
 | 
					  Title,
 | 
				
			||||||
  Tree,
 | 
					 | 
				
			||||||
} from '@mantine/core'
 | 
					} from '@mantine/core'
 | 
				
			||||||
 | 
					import { notifications } from '@mantine/notifications'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  IconChevronDown,
 | 
				
			||||||
 | 
					  IconDownload,
 | 
				
			||||||
  IconFileTypeDocx,
 | 
					  IconFileTypeDocx,
 | 
				
			||||||
  IconFileTypePdf,
 | 
					  IconFileTypePdf,
 | 
				
			||||||
  IconFolder,
 | 
					  IconFolder,
 | 
				
			||||||
  IconFolderOpen,
 | 
					 | 
				
			||||||
  IconFolderX,
 | 
					 | 
				
			||||||
  IconListCheck,
 | 
					  IconListCheck,
 | 
				
			||||||
  IconPhoto,
 | 
					  IconPhoto,
 | 
				
			||||||
 | 
					  IconSearch,
 | 
				
			||||||
 | 
					  IconTrash,
 | 
				
			||||||
} from '@tabler/icons-react'
 | 
					} from '@tabler/icons-react'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import { useEffect, useState } from 'react'
 | 
				
			||||||
 | 
					import FileUploadForm from '../Profile/components/FileUploadForm'
 | 
				
			||||||
import classes from './AllProfiles.module.css'
 | 
					import classes from './AllProfiles.module.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileIconProps {
 | 
					interface FileData {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
  name: string
 | 
					  name: string
 | 
				
			||||||
  isFolder: boolean
 | 
					  url: string
 | 
				
			||||||
  expanded: boolean
 | 
					  type: string
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  created_at: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TFileProfile = {
 | 
					interface GroupedFiles {
 | 
				
			||||||
  label: string
 | 
					  [key: string]: FileData[]
 | 
				
			||||||
  type: string
 | 
					 | 
				
			||||||
  value: string
 | 
					 | 
				
			||||||
  children?: TFileProfile[]
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AllProfiles = () => {
 | 
					const AllProfiles = () => {
 | 
				
			||||||
  const [treeData, setTreeData] = useState([])
 | 
					  const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
 | 
				
			||||||
  const [cv, setCv] = useState<File>()
 | 
					  const [currentUser, setCurrentUser] = useState<string>('')
 | 
				
			||||||
  const [idCard, setIdCard] = useState<File>()
 | 
					 | 
				
			||||||
  const [transcript, setTranscript] = useState<File>()
 | 
					 | 
				
			||||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
					 | 
				
			||||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
					 | 
				
			||||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
					 | 
				
			||||||
  const [currentName, setCurrentName] = useState<string>('')
 | 
					 | 
				
			||||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
					  const [openedProfile, setOpenedProfile] = useState(false)
 | 
				
			||||||
  function FileIcon({ name, isFolder, expanded }: FileIconProps) {
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
				
			||||||
    if (name.endsWith('.pdf')) {
 | 
					  const [isLoading, setIsLoading] = useState(false)
 | 
				
			||||||
      return <IconFileTypePdf size={14} />
 | 
					  const [expandedFolders, setExpandedFolders] = useState<{
 | 
				
			||||||
    }
 | 
					    [key: string]: boolean
 | 
				
			||||||
 | 
					  }>({})
 | 
				
			||||||
 | 
					  const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
					  const toggleFolder = (userName: string) => {
 | 
				
			||||||
      return <IconFileTypeDocx size={14} />
 | 
					    setExpandedFolders((prev) => ({
 | 
				
			||||||
    }
 | 
					      ...prev,
 | 
				
			||||||
 | 
					      [userName]: !prev[userName],
 | 
				
			||||||
    if (
 | 
					    }))
 | 
				
			||||||
      name.endsWith('.jpg') ||
 | 
					 | 
				
			||||||
      name.endsWith('.png') ||
 | 
					 | 
				
			||||||
      name.endsWith('.jpeg') ||
 | 
					 | 
				
			||||||
      name.endsWith('.webp')
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return <IconPhoto size={14} />
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isFolder) {
 | 
					 | 
				
			||||||
      return expanded ? (
 | 
					 | 
				
			||||||
        <IconFolderOpen
 | 
					 | 
				
			||||||
          color="var(--mantine-color-yellow-9)"
 | 
					 | 
				
			||||||
          size={14}
 | 
					 | 
				
			||||||
          stroke={2.5}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      ) : (
 | 
					 | 
				
			||||||
        <IconFolder
 | 
					 | 
				
			||||||
          color="var(--mantine-color-yellow-9)"
 | 
					 | 
				
			||||||
          size={14}
 | 
					 | 
				
			||||||
          stroke={2.5}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <IconFolderX color="var(--mantine-color-red-9)" size={14} stroke={2.5} />
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function Leaf({
 | 
					  const getFileIcon = (type: string) => {
 | 
				
			||||||
    node,
 | 
					    switch (type) {
 | 
				
			||||||
    expanded,
 | 
					      case 'document':
 | 
				
			||||||
    hasChildren,
 | 
					        return <IconFileTypeDocx size={16} />
 | 
				
			||||||
    elementProps,
 | 
					      case 'image':
 | 
				
			||||||
  }: RenderTreeNodePayload) {
 | 
					        return <IconPhoto size={16} />
 | 
				
			||||||
    return (
 | 
					      default:
 | 
				
			||||||
      <Group gap={5} {...elementProps}>
 | 
					        return <IconFileTypePdf size={16} />
 | 
				
			||||||
        {!node.children ? (
 | 
					    }
 | 
				
			||||||
          <a href={node.value} target="_blank">
 | 
					 | 
				
			||||||
            <FileIcon
 | 
					 | 
				
			||||||
              name={node.value}
 | 
					 | 
				
			||||||
              isFolder={hasChildren}
 | 
					 | 
				
			||||||
              expanded={expanded}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span>{node.label}</span>
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <>
 | 
					 | 
				
			||||||
            <FileIcon
 | 
					 | 
				
			||||||
              name={node.value}
 | 
					 | 
				
			||||||
              isFolder={hasChildren}
 | 
					 | 
				
			||||||
              expanded={expanded}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span>{node.label}</span>
 | 
					 | 
				
			||||||
            <Tooltip label="Upload">
 | 
					 | 
				
			||||||
              <IconListCheck
 | 
					 | 
				
			||||||
                color="green"
 | 
					 | 
				
			||||||
                width={15}
 | 
					 | 
				
			||||||
                height={15}
 | 
					 | 
				
			||||||
                style={{
 | 
					 | 
				
			||||||
                  display: node.label !== 'others' ? 'block' : 'none',
 | 
					 | 
				
			||||||
                  cursor: 'pointer',
 | 
					 | 
				
			||||||
                  zIndex: 100000,
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onClick={() => {
 | 
					 | 
				
			||||||
                  setCurrentName(node.label!.toString())
 | 
					 | 
				
			||||||
                  setOpenedProfile(true)
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </Tooltip>
 | 
					 | 
				
			||||||
          </>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </Group>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleOtherFileChange = (
 | 
					  const handleSubmit = async (
 | 
				
			||||||
    index: number,
 | 
					    e: React.FormEvent,
 | 
				
			||||||
    field: string,
 | 
					    fileName: string,
 | 
				
			||||||
    value: File | string,
 | 
					    description: string,
 | 
				
			||||||
 | 
					    currentUser: string
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    const updatedFiles: any = [...otherFiles]
 | 
					 | 
				
			||||||
    updatedFiles[index][field] = value
 | 
					 | 
				
			||||||
    setOtherFiles(updatedFiles)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const addOtherFileInput = () => {
 | 
					 | 
				
			||||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleSubmit = async (e: any) => {
 | 
					 | 
				
			||||||
    e.preventDefault()
 | 
					    e.preventDefault()
 | 
				
			||||||
 | 
					    setIsLoading(true)
 | 
				
			||||||
    const formData = new FormData()
 | 
					    const formData = new FormData()
 | 
				
			||||||
 | 
					    if (selectedFile) {
 | 
				
			||||||
 | 
					      formData.append('file', selectedFile)
 | 
				
			||||||
 | 
					      formData.append('name', fileName)
 | 
				
			||||||
 | 
					      formData.append('description', description)
 | 
				
			||||||
 | 
					      formData.append('user_name', currentUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Append each selected file to FormData
 | 
					      try {
 | 
				
			||||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
					        const token = await getAccessToken()
 | 
				
			||||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
					        const response = await axios.post(uploadFiles, formData, {
 | 
				
			||||||
        formData.append(
 | 
					          headers: {
 | 
				
			||||||
          'files[]',
 | 
					            'Content-Type': 'multipart/form-data',
 | 
				
			||||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
					            Authorization: `Bearer ${token}`,
 | 
				
			||||||
        )
 | 
					          },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.status === 200) {
 | 
				
			||||||
 | 
					          setSelectedFile(null)
 | 
				
			||||||
 | 
					          await getAllFiles()
 | 
				
			||||||
 | 
					          return true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error uploading file:', error)
 | 
				
			||||||
 | 
					        throw error
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsLoading(false)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (cv) {
 | 
					  const getAllFiles = async () => {
 | 
				
			||||||
      formData.append('files[]', cv)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (idCard) {
 | 
					 | 
				
			||||||
      formData.append('files[]', idCard)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (transcript) {
 | 
					 | 
				
			||||||
      formData.append('files[]', transcript)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (universityDiploma) {
 | 
					 | 
				
			||||||
      formData.append('files[]', universityDiploma)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    formData.append('name', currentName)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const token = await getAccessToken()
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
					      const res = await get(getFiles)
 | 
				
			||||||
 | 
					      if (res.status === true) {
 | 
				
			||||||
 | 
					        setGroupedFiles(res.data)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(error)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const removeFile = async (id: number) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const token = await getAccessToken();
 | 
				
			||||||
 | 
					      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
				
			||||||
        headers: {
 | 
					        headers: {
 | 
				
			||||||
          'Content-Type': 'multipart/form-data',
 | 
					 | 
				
			||||||
          Authorization: `Bearer ${token}`,
 | 
					          Authorization: `Bearer ${token}`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      })
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (response.status === 200) {
 | 
					      if (response.status === 200) {
 | 
				
			||||||
        getAllFile()
 | 
					        notifications.show({
 | 
				
			||||||
        getTree()
 | 
					          title: 'Thành công',
 | 
				
			||||||
        setOtherFiles([])
 | 
					          message: 'Xóa file thành công',
 | 
				
			||||||
 | 
					          color: 'green',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await getAllFiles();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Error uploading files', error)
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Lỗi',
 | 
				
			||||||
 | 
					        message: 'Không thể xóa file',
 | 
				
			||||||
 | 
					        color: 'red',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getAllFile = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const res = await get(getAllFilesInProfiles, {
 | 
					 | 
				
			||||||
        root_folder: '/storage/profiles/' + currentName,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      if (res.status === true) {
 | 
					 | 
				
			||||||
        setData(res.data)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.log(error)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const removeFile = async (url: string) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await Xdelete(deleteFile, { file_url: url }, getAllFile)
 | 
					 | 
				
			||||||
      getTree()
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.log(error)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const getTree = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const res = await get(getAllFilesInProfiles, {
 | 
					 | 
				
			||||||
        root_folder: '/storage/profiles',
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      if (res.status === true) {
 | 
					 | 
				
			||||||
        setTreeData(res.data)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.log(error)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
					 | 
				
			||||||
    const originalFile = e // Get the original file
 | 
					 | 
				
			||||||
    const extend = originalFile.name.split('.')[1]
 | 
					 | 
				
			||||||
    if (originalFile) {
 | 
					 | 
				
			||||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
					 | 
				
			||||||
      const newFile = new File([originalFile], newFileName, {
 | 
					 | 
				
			||||||
        type: originalFile.type,
 | 
					 | 
				
			||||||
      }) // Create new file object
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return newFile // Save the new file object for further processing
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const checkFileExist = (nameField: string) => {
 | 
					 | 
				
			||||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
					 | 
				
			||||||
    return file
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    getTree()
 | 
					    getAllFiles()
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  const filterFiles = (files: FileData[], searchTerm: string) => {
 | 
				
			||||||
    getAllFile()
 | 
					    return files.filter(
 | 
				
			||||||
  }, [currentName])
 | 
					      (file) =>
 | 
				
			||||||
 | 
					        file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
				
			||||||
 | 
					        (file.description &&
 | 
				
			||||||
 | 
					          file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <div className={classes.title}>
 | 
					      <div className={classes.title}>
 | 
				
			||||||
        <h3>
 | 
					        <h3>
 | 
				
			||||||
          <Text>Admin/</Text>
 | 
					          <Text size="sm">Admin/</Text>
 | 
				
			||||||
          Profiles
 | 
					          Files Management
 | 
				
			||||||
        </h3>
 | 
					        </h3>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <Box ml={'lg'}>
 | 
					      <Box ml={'md'}>
 | 
				
			||||||
        <Tree
 | 
					        <Stack gap="xs">
 | 
				
			||||||
          classNames={classes}
 | 
					          {Object.entries(groupedFiles).map(([userName, files]) => (
 | 
				
			||||||
          selectOnClick
 | 
					            <Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
 | 
				
			||||||
          clearSelectionOnOutsideClick
 | 
					              <Group
 | 
				
			||||||
          data={treeData}
 | 
					                justify="space-between"
 | 
				
			||||||
          renderNode={(payload) => <Leaf {...payload} />}
 | 
					                mb="xs"
 | 
				
			||||||
        />
 | 
					                gap="xs"
 | 
				
			||||||
 | 
					                onClick={() => toggleFolder(userName)}
 | 
				
			||||||
 | 
					                style={{ cursor: 'pointer' }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Group gap="xs">
 | 
				
			||||||
 | 
					                  <IconFolder size={18} color="var(--mantine-color-yellow-9)" />
 | 
				
			||||||
 | 
					                  <Title order={5}>{userName}</Title>
 | 
				
			||||||
 | 
					                </Group>
 | 
				
			||||||
 | 
					                <Group gap="xs">
 | 
				
			||||||
 | 
					                  <Button
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    variant="subtle"
 | 
				
			||||||
 | 
					                    color="gray"
 | 
				
			||||||
 | 
					                    onClick={() => toggleFolder(userName)}
 | 
				
			||||||
 | 
					                    leftSection={
 | 
				
			||||||
 | 
					                      <IconChevronDown
 | 
				
			||||||
 | 
					                        size={14}
 | 
				
			||||||
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          transform: expandedFolders[userName]
 | 
				
			||||||
 | 
					                            ? 'rotate(180deg)'
 | 
				
			||||||
 | 
					                            : 'none',
 | 
				
			||||||
 | 
					                          transition: 'transform 0.2s',
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {expandedFolders[userName] ? 'Collapse' : 'Expand'}
 | 
				
			||||||
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                  <Button
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    variant="light"
 | 
				
			||||||
 | 
					                    color="blue"
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      setCurrentUser(userName)
 | 
				
			||||||
 | 
					                      setOpenedProfile(true)
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Group gap={2}>
 | 
				
			||||||
 | 
					                      <IconListCheck size={14} />
 | 
				
			||||||
 | 
					                      <Text size="xs">Upload Files</Text>
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                </Group>
 | 
				
			||||||
 | 
					              </Group>
 | 
				
			||||||
 | 
					              <Collapse in={expandedFolders[userName]}>
 | 
				
			||||||
 | 
					                <Stack gap="xs">
 | 
				
			||||||
 | 
					                  <TextInput
 | 
				
			||||||
 | 
					                    placeholder="Search files by name or description..."
 | 
				
			||||||
 | 
					                    size="xs"
 | 
				
			||||||
 | 
					                    leftSection={<IconSearch size={14} />}
 | 
				
			||||||
 | 
					                    value={searchTerms[userName] || ''}
 | 
				
			||||||
 | 
					                    onChange={(e) =>
 | 
				
			||||||
 | 
					                      setSearchTerms((prev) => ({
 | 
				
			||||||
 | 
					                        ...prev,
 | 
				
			||||||
 | 
					                        [userName]: e.target.value,
 | 
				
			||||||
 | 
					                      }))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    onClick={(e) => e.stopPropagation()}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                  {filterFiles(files, searchTerms[userName] || '')
 | 
				
			||||||
 | 
					                    .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
 | 
				
			||||||
 | 
					                    .map((file: FileData) => (
 | 
				
			||||||
 | 
					                      <Card
 | 
				
			||||||
 | 
					                        key={file.id}
 | 
				
			||||||
 | 
					                        shadow="xs"
 | 
				
			||||||
 | 
					                        padding="xs"
 | 
				
			||||||
 | 
					                        radius="sm"
 | 
				
			||||||
 | 
					                        withBorder
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Group justify="space-between" gap="xs">
 | 
				
			||||||
 | 
					                          <Group gap="xs">
 | 
				
			||||||
 | 
					                            {getFileIcon(file.type)}
 | 
				
			||||||
 | 
					                            <Box>
 | 
				
			||||||
 | 
					                              <Text size="xs" fw={500}>
 | 
				
			||||||
 | 
					                                {file.name}
 | 
				
			||||||
 | 
					                              </Text>
 | 
				
			||||||
 | 
					                              {file.description && (
 | 
				
			||||||
 | 
					                                <Text size="xs" c="dimmed">
 | 
				
			||||||
 | 
					                                  {file.description}
 | 
				
			||||||
 | 
					                                </Text>
 | 
				
			||||||
 | 
					                              )}
 | 
				
			||||||
 | 
					                              <Text size="xs" c="dimmed">
 | 
				
			||||||
 | 
					                                Uploaded:{' '}
 | 
				
			||||||
 | 
					                                {new Date(file.created_at).toLocaleDateString()}
 | 
				
			||||||
 | 
					                              </Text>
 | 
				
			||||||
 | 
					                            </Box>
 | 
				
			||||||
 | 
					                          </Group>
 | 
				
			||||||
 | 
					                          <Group gap="xs">
 | 
				
			||||||
 | 
					                            <Button
 | 
				
			||||||
 | 
					                              size="xs"
 | 
				
			||||||
 | 
					                              variant="light" 
 | 
				
			||||||
 | 
					                              color="blue"
 | 
				
			||||||
 | 
					                              component='a'
 | 
				
			||||||
 | 
					                              href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
				
			||||||
 | 
					                                import.meta.env.VITE_BACKEND_URL?.includes(
 | 
				
			||||||
 | 
					                                  'localhost',
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                  ? ''
 | 
				
			||||||
 | 
					                                  : 'image/'
 | 
				
			||||||
 | 
					                              }${file.url.slice(1)}`}
 | 
				
			||||||
 | 
					                              target="_blank"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              <Group gap={2}>
 | 
				
			||||||
 | 
					                                <IconDownload size={12} />
 | 
				
			||||||
 | 
					                                <Text size="xs">Download</Text>
 | 
				
			||||||
 | 
					                              </Group>
 | 
				
			||||||
 | 
					                            </Button>
 | 
				
			||||||
 | 
					                            <Button
 | 
				
			||||||
 | 
					                              size="xs"
 | 
				
			||||||
 | 
					                              variant="light"
 | 
				
			||||||
 | 
					                              color="red"
 | 
				
			||||||
 | 
					                              onClick={() => removeFile(file.id)}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              <Group gap={2}>
 | 
				
			||||||
 | 
					                                <IconTrash size={12} />
 | 
				
			||||||
 | 
					                                <Text size="xs">Delete</Text>
 | 
				
			||||||
 | 
					                              </Group>
 | 
				
			||||||
 | 
					                            </Button>
 | 
				
			||||||
 | 
					                          </Group>
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					                      </Card>
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </Stack>
 | 
				
			||||||
 | 
					              </Collapse>
 | 
				
			||||||
 | 
					            </Card>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </Stack>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Modal
 | 
					        <Modal
 | 
				
			||||||
          size={'lg'}
 | 
					          size="lg"
 | 
				
			||||||
          opened={openedProfile}
 | 
					          opened={openedProfile}
 | 
				
			||||||
          onClose={() => {
 | 
					          onClose={() => {
 | 
				
			||||||
            setOpenedProfile(false)
 | 
					            setOpenedProfile(false)
 | 
				
			||||||
 | 
					            setCurrentUser('')
 | 
				
			||||||
 | 
					            setSelectedFile(null)
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Box>
 | 
					          <Box>
 | 
				
			||||||
            <form onSubmit={handleSubmit}>
 | 
					            <FileUploadForm
 | 
				
			||||||
              <Stack>
 | 
					              data={groupedFiles[currentUser] || []}
 | 
				
			||||||
                <Box
 | 
					              handleSubmit={handleSubmit}
 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
					              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
				
			||||||
                >
 | 
					              removeFile={removeFile}
 | 
				
			||||||
                  <Text>CV</Text>
 | 
					              isLoading={isLoading}
 | 
				
			||||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
					              currentUser={currentUser}
 | 
				
			||||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
					            />
 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('cv')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label={'CV'}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CV"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    0
 | 
					 | 
				
			||||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.doc,.docx"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>CCCD</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('idCard')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="CCCD"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'none' : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CCCD"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bảng điểm</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('transcript')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bảng điểm"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'none' : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bảng điểm"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'flex'
 | 
					 | 
				
			||||||
                      : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bằng đại học</Text>
 | 
					 | 
				
			||||||
                  <a
 | 
					 | 
				
			||||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <Text>{`: ${
 | 
					 | 
				
			||||||
                      checkFileExist('universityDiploma')?.label
 | 
					 | 
				
			||||||
                    }`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('universityDiploma')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bằng đại học"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'none'
 | 
					 | 
				
			||||||
                      : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bằng đại học"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setUniversityDiploma(
 | 
					 | 
				
			||||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Text>Danh sách file khác:</Text>
 | 
					 | 
				
			||||||
                <Box>
 | 
					 | 
				
			||||||
                  {data
 | 
					 | 
				
			||||||
                    .find((f) => f.label === 'others')
 | 
					 | 
				
			||||||
                    ?.children?.map((c, index) => {
 | 
					 | 
				
			||||||
                      return (
 | 
					 | 
				
			||||||
                        <Box
 | 
					 | 
				
			||||||
                          key={index}
 | 
					 | 
				
			||||||
                          style={{
 | 
					 | 
				
			||||||
                            display: 'flex',
 | 
					 | 
				
			||||||
                            marginBottom: '5px',
 | 
					 | 
				
			||||||
                          }}
 | 
					 | 
				
			||||||
                        >
 | 
					 | 
				
			||||||
                          <a href={c?.value} target="_blank">
 | 
					 | 
				
			||||||
                            <Text>{`${c?.label}`}</Text>
 | 
					 | 
				
			||||||
                          </a>
 | 
					 | 
				
			||||||
                          <Button
 | 
					 | 
				
			||||||
                            variant="outline"
 | 
					 | 
				
			||||||
                            size="xs"
 | 
					 | 
				
			||||||
                            color="red"
 | 
					 | 
				
			||||||
                            ml={'sm'}
 | 
					 | 
				
			||||||
                            onClick={() => {
 | 
					 | 
				
			||||||
                              removeFile(c?.value!)
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                          >
 | 
					 | 
				
			||||||
                            Delete
 | 
					 | 
				
			||||||
                          </Button>
 | 
					 | 
				
			||||||
                        </Box>
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                {otherFiles.map((fileInput, index) => (
 | 
					 | 
				
			||||||
                  <Group key={index}>
 | 
					 | 
				
			||||||
                    <FileInput
 | 
					 | 
				
			||||||
                      placeholder="Chọn file"
 | 
					 | 
				
			||||||
                      onChange={(file) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(index, 'file', file!)
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      w={'30%'}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                    <TextInput
 | 
					 | 
				
			||||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
					 | 
				
			||||||
                      value={fileInput.type}
 | 
					 | 
				
			||||||
                      w={'65%'}
 | 
					 | 
				
			||||||
                      onChange={(e) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(
 | 
					 | 
				
			||||||
                          index,
 | 
					 | 
				
			||||||
                          'type',
 | 
					 | 
				
			||||||
                          e.currentTarget.value,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </Group>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
					 | 
				
			||||||
                  Thêm file khác
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Button type="submit" color="blue">
 | 
					 | 
				
			||||||
                  Save
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
              </Stack>
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Modal>
 | 
					        </Modal>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,8 +103,12 @@ const Document = () => {
 | 
				
			||||||
    if (['doc'].includes(extension!)) {
 | 
					    if (['doc'].includes(extension!)) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <a
 | 
					        <a
 | 
				
			||||||
          href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
 | 
					          href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
				
			||||||
          download="Document Download"
 | 
					            import.meta.env.VITE_BACKEND_URL?.includes('localhost')
 | 
				
			||||||
 | 
					              ? ''
 | 
				
			||||||
 | 
					              : 'image/'
 | 
				
			||||||
 | 
					          }${uri}`}
 | 
				
			||||||
 | 
					          download={row.title}
 | 
				
			||||||
          target="_self"
 | 
					          target="_self"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <IconFileTypeDoc style={{ color: '#1e62c1' }} />
 | 
					          <IconFileTypeDoc style={{ color: '#1e62c1' }} />
 | 
				
			||||||
| 
						 | 
					@ -115,8 +119,12 @@ const Document = () => {
 | 
				
			||||||
    if (['xls', 'xlsx'].includes(extension!)) {
 | 
					    if (['xls', 'xlsx'].includes(extension!)) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <a
 | 
					        <a
 | 
				
			||||||
          href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
 | 
					          href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
				
			||||||
          download="Document Download"
 | 
					            import.meta.env.VITE_BACKEND_URL?.includes('localhost')
 | 
				
			||||||
 | 
					              ? ''
 | 
				
			||||||
 | 
					              : 'image/'
 | 
				
			||||||
 | 
					          }${uri}`}
 | 
				
			||||||
 | 
					          download={row.title}
 | 
				
			||||||
          target="_self"
 | 
					          target="_self"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <IconFileTypeXls style={{ color: '#0e864b' }} />
 | 
					          <IconFileTypeXls style={{ color: '#0e864b' }} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,9 @@
 | 
				
			||||||
import { getLeaveManagement, updateNoteLeave, exportLeaveManagement } from '@/api/Admin'
 | 
					import {
 | 
				
			||||||
 | 
					  getLeaveManagement,
 | 
				
			||||||
 | 
					  updateNoteLeave,
 | 
				
			||||||
 | 
					  exportLeaveManagement,
 | 
				
			||||||
 | 
					  // getListMaster,
 | 
				
			||||||
 | 
					} from '@/api/Admin'
 | 
				
			||||||
import { update } from '@/rtk/helpers/CRUD'
 | 
					import { update } from '@/rtk/helpers/CRUD'
 | 
				
			||||||
import { get, exportFile } from '@/rtk/helpers/apiService'
 | 
					import { get, exportFile } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -40,8 +45,9 @@ interface LeaveDay {
 | 
				
			||||||
  id: number
 | 
					  id: number
 | 
				
			||||||
  ld_user_id: number
 | 
					  ld_user_id: number
 | 
				
			||||||
  ld_year: number
 | 
					  ld_year: number
 | 
				
			||||||
  ld_day: number
 | 
					  ld_day_total: number
 | 
				
			||||||
  ld_date_additional: number
 | 
					  ld_additional_day: number
 | 
				
			||||||
 | 
					  ld_special_leave_day: number
 | 
				
			||||||
  ld_note: string
 | 
					  ld_note: string
 | 
				
			||||||
  created_at: string | null
 | 
					  created_at: string | null
 | 
				
			||||||
  updated_at: string | null
 | 
					  updated_at: string | null
 | 
				
			||||||
| 
						 | 
					@ -52,6 +58,7 @@ interface MonthlyLeaveDays {
 | 
				
			||||||
  leave_days: number
 | 
					  leave_days: number
 | 
				
			||||||
  month: number
 | 
					  month: number
 | 
				
			||||||
  n_user_id: number
 | 
					  n_user_id: number
 | 
				
			||||||
 | 
					  reason_code: string
 | 
				
			||||||
  reason_name: string
 | 
					  reason_name: string
 | 
				
			||||||
  time_type_name: string
 | 
					  time_type_name: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -62,6 +69,18 @@ interface UserData {
 | 
				
			||||||
  monthlyLeaveDays: MonthlyLeaveDays[]
 | 
					  monthlyLeaveDays: MonthlyLeaveDays[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// interface DataReason {
 | 
				
			||||||
 | 
					//   id: number
 | 
				
			||||||
 | 
					//   c_code: string
 | 
				
			||||||
 | 
					//   c_name: string
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// interface DataTimeType {
 | 
				
			||||||
 | 
					//   id: number
 | 
				
			||||||
 | 
					//   c_code: string
 | 
				
			||||||
 | 
					//   c_name: string
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LeaveManagement = () => {
 | 
					const LeaveManagement = () => {
 | 
				
			||||||
  const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
 | 
					  const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
 | 
				
			||||||
  const [disableBtn, setDisableBtn] = useState(false)
 | 
					  const [disableBtn, setDisableBtn] = useState(false)
 | 
				
			||||||
| 
						 | 
					@ -75,6 +94,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
    note: string
 | 
					    note: string
 | 
				
			||||||
    totalLeave: string
 | 
					    totalLeave: string
 | 
				
			||||||
    dayAdditional: string
 | 
					    dayAdditional: string
 | 
				
			||||||
 | 
					    specialLeave: string
 | 
				
			||||||
  }>({
 | 
					  }>({
 | 
				
			||||||
    id: 0,
 | 
					    id: 0,
 | 
				
			||||||
    user: {
 | 
					    user: {
 | 
				
			||||||
| 
						 | 
					@ -84,12 +104,53 @@ const LeaveManagement = () => {
 | 
				
			||||||
    note: '',
 | 
					    note: '',
 | 
				
			||||||
    totalLeave: '',
 | 
					    totalLeave: '',
 | 
				
			||||||
    dayAdditional: '',
 | 
					    dayAdditional: '',
 | 
				
			||||||
 | 
					    specialLeave: '',
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [data, setData] = useState<UserData[]>([])
 | 
					  const [data, setData] = useState<UserData[]>([])
 | 
				
			||||||
  const [date, setDate] = useState({
 | 
					  const [date, setDate] = useState({
 | 
				
			||||||
    year: new Date().getFullYear().toString(),
 | 
					    year: new Date().getFullYear().toString(),
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					  // const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
 | 
				
			||||||
 | 
					  // const [dataReason, setDataReason] = useState<DataReason[]>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const now = new Date()
 | 
				
			||||||
 | 
					  const currentMonth = now.getMonth() + 1 // getMonth() trả về 0-11
 | 
				
			||||||
 | 
					  const currentYear = now.getFullYear()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // const getListMasterByType = async (type: string) => {
 | 
				
			||||||
 | 
					  //   try {
 | 
				
			||||||
 | 
					  //     const params = {
 | 
				
			||||||
 | 
					  //       type: type,
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //     const res = await get(getListMaster, params)
 | 
				
			||||||
 | 
					  //     if (res.status) {
 | 
				
			||||||
 | 
					  //       return res.data
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //   } catch (error: any) {
 | 
				
			||||||
 | 
					  //     notifications.show({
 | 
				
			||||||
 | 
					  //       title: 'Error',
 | 
				
			||||||
 | 
					  //       message: error.message ?? error,
 | 
				
			||||||
 | 
					  //       color: 'red',
 | 
				
			||||||
 | 
					  //     })
 | 
				
			||||||
 | 
					  //   }
 | 
				
			||||||
 | 
					  //   return []
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // useEffect(() => {
 | 
				
			||||||
 | 
					  //   const fetchData = async () => {
 | 
				
			||||||
 | 
					  //     const resultTimeType = await getListMasterByType('TIME_TYPE')
 | 
				
			||||||
 | 
					  //     setDataTimeType(
 | 
				
			||||||
 | 
					  //       resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
 | 
				
			||||||
 | 
					  //     )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //     const resultReason = await getListMasterByType('REASON')
 | 
				
			||||||
 | 
					  //     setDataReason(resultReason)
 | 
				
			||||||
 | 
					  //   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //   fetchData()
 | 
				
			||||||
 | 
					  // }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getLeaveList = async () => {
 | 
					  const getLeaveList = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await get(getLeaveManagement, {
 | 
					      const res = await get(getLeaveManagement, {
 | 
				
			||||||
| 
						 | 
					@ -122,6 +183,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    totalLeave: string,
 | 
					    totalLeave: string,
 | 
				
			||||||
    dayAdditional: string,
 | 
					    dayAdditional: string,
 | 
				
			||||||
 | 
					    specialLeave: string,
 | 
				
			||||||
    note: string,
 | 
					    note: string,
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
| 
						 | 
					@ -132,6 +194,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
          users: users,
 | 
					          users: users,
 | 
				
			||||||
          totalLeave: totalLeave,
 | 
					          totalLeave: totalLeave,
 | 
				
			||||||
          dayAdditional: dayAdditional,
 | 
					          dayAdditional: dayAdditional,
 | 
				
			||||||
 | 
					          specialLeave: specialLeave,
 | 
				
			||||||
          note: note,
 | 
					          note: note,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        getLeaveList,
 | 
					        getLeaveList,
 | 
				
			||||||
| 
						 | 
					@ -237,8 +300,8 @@ const LeaveManagement = () => {
 | 
				
			||||||
        <div key={indexDay}>
 | 
					        <div key={indexDay}>
 | 
				
			||||||
          {isNewMonth && <p>Month {lastmonth}</p>}
 | 
					          {isNewMonth && <p>Month {lastmonth}</p>}
 | 
				
			||||||
          <p style={{ paddingLeft: '20px' }}>
 | 
					          <p style={{ paddingLeft: '20px' }}>
 | 
				
			||||||
            - {itemDay.reason_name} ({itemDay.time_type_name}) {itemDay.day}
 | 
					            - {itemDay.reason_name} ({itemDay.time_type_name}) {itemDay.day}/
 | 
				
			||||||
            /{itemDay.month}
 | 
					            {itemDay.month}
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					@ -253,11 +316,10 @@ const LeaveManagement = () => {
 | 
				
			||||||
      await exportFile(
 | 
					      await exportFile(
 | 
				
			||||||
        exportLeaveManagement,
 | 
					        exportLeaveManagement,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          year: parseInt(date.year)
 | 
					          year: parseInt(date.year),
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        fileName
 | 
					        fileName,
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Export error:', error)
 | 
					      console.error('Export error:', error)
 | 
				
			||||||
      notifications.show({
 | 
					      notifications.show({
 | 
				
			||||||
| 
						 | 
					@ -271,9 +333,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <div className={classes.title}>
 | 
					      <div className={classes.title}>
 | 
				
			||||||
        <h3>
 | 
					        <h3>Leave Management</h3>
 | 
				
			||||||
          Leave Management
 | 
					 | 
				
			||||||
        </h3>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <Drawer
 | 
					      <Drawer
 | 
				
			||||||
        opened={opened1}
 | 
					        opened={opened1}
 | 
				
			||||||
| 
						 | 
					@ -305,8 +365,8 @@ const LeaveManagement = () => {
 | 
				
			||||||
              })
 | 
					              })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          label={'Total Leave'}
 | 
					          label={'Phép năm'}
 | 
				
			||||||
          placeholder="Input placeholder"
 | 
					          placeholder="Nhập số ngày phép năm"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <TextInput
 | 
					        <TextInput
 | 
				
			||||||
          mb={'md'}
 | 
					          mb={'md'}
 | 
				
			||||||
| 
						 | 
					@ -332,10 +392,36 @@ const LeaveManagement = () => {
 | 
				
			||||||
              })
 | 
					              })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          label={'Day additional leave'}
 | 
					          label={'Phép năm cũ'}
 | 
				
			||||||
          placeholder="Input placeholder"
 | 
					          placeholder="Nhập số ngày phép năm cũ"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          mb={'md'}
 | 
				
			||||||
 | 
					          value={customAddNotes.specialLeave}
 | 
				
			||||||
 | 
					          onChange={(e) => {
 | 
				
			||||||
 | 
					            const value = e.target.value
 | 
				
			||||||
 | 
					            if (value) {
 | 
				
			||||||
 | 
					              const floatValue = parseFloat(value)
 | 
				
			||||||
 | 
					              if (
 | 
				
			||||||
 | 
					                /^\d*\.?\d?$/.test(value) &&
 | 
				
			||||||
 | 
					                floatValue >= 0 &&
 | 
				
			||||||
 | 
					                floatValue <= 20
 | 
				
			||||||
 | 
					              ) {
 | 
				
			||||||
 | 
					                setCustomAddNotes({
 | 
				
			||||||
 | 
					                  ...customAddNotes,
 | 
				
			||||||
 | 
					                  specialLeave: value,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              setCustomAddNotes({
 | 
				
			||||||
 | 
					                ...customAddNotes,
 | 
				
			||||||
 | 
					                specialLeave: '',
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					          label={'Phép đặc biệt'}
 | 
				
			||||||
 | 
					          placeholder="Nhập số ngày phép đặc biệt"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <Textarea
 | 
					        <Textarea
 | 
				
			||||||
          mb={'md'}
 | 
					          mb={'md'}
 | 
				
			||||||
          label="Note"
 | 
					          label="Note"
 | 
				
			||||||
| 
						 | 
					@ -368,6 +454,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
                customAddNotes.user,
 | 
					                customAddNotes.user,
 | 
				
			||||||
                customAddNotes.totalLeave,
 | 
					                customAddNotes.totalLeave,
 | 
				
			||||||
                customAddNotes.dayAdditional,
 | 
					                customAddNotes.dayAdditional,
 | 
				
			||||||
 | 
					                customAddNotes.specialLeave,
 | 
				
			||||||
                customAddNotes.note,
 | 
					                customAddNotes.note,
 | 
				
			||||||
              )
 | 
					              )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -439,15 +526,23 @@ const LeaveManagement = () => {
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Table.Thead>
 | 
					          <Table.Thead>
 | 
				
			||||||
            <Table.Tr bg={'#228be66b'}>
 | 
					            <Table.Tr bg={'#228be66b'}>
 | 
				
			||||||
              <Table.Th ></Table.Th>
 | 
					              <Table.Th ta={'center'} style={{ width: '40px' }}></Table.Th>
 | 
				
			||||||
              <Table.Th>User</Table.Th>
 | 
					              <Table.Th>User</Table.Th>
 | 
				
			||||||
              {monthInYear.map((d) => {
 | 
					              {monthInYear.map((d) => {
 | 
				
			||||||
 | 
					                const isCurrentMonth =
 | 
				
			||||||
 | 
					                  Number(date.year) === currentYear && d.value === currentMonth
 | 
				
			||||||
                return (
 | 
					                return (
 | 
				
			||||||
                  <Menu width={200} shadow="md" key={d.value}>
 | 
					                  <Menu width={200} shadow="md" key={d.value}>
 | 
				
			||||||
                    <Menu.Target>
 | 
					                    <Menu.Target>
 | 
				
			||||||
                      <Table.Th
 | 
					                      <Table.Th
 | 
				
			||||||
                        ta={'center'}
 | 
					                        ta={'center'}
 | 
				
			||||||
                        style={{ cursor: 'pointer', width: '60px' }}
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          cursor: 'pointer',
 | 
				
			||||||
 | 
					                          width: '40px',
 | 
				
			||||||
 | 
					                          backgroundColor: isCurrentMonth ? '#ffe066' : undefined,
 | 
				
			||||||
 | 
					                          color: isCurrentMonth ? '#000' : undefined,
 | 
				
			||||||
 | 
					                          fontWeight: isCurrentMonth ? 'bold' : undefined,
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
                        <span>{d.name}</span>
 | 
					                        <span>{d.name}</span>
 | 
				
			||||||
                      </Table.Th>
 | 
					                      </Table.Th>
 | 
				
			||||||
| 
						 | 
					@ -455,10 +550,10 @@ const LeaveManagement = () => {
 | 
				
			||||||
                  </Menu>
 | 
					                  </Menu>
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
              <Table.Th ta={'center'} style={{ width: '80px' }}>
 | 
					              <Table.Th ta={'center'} style={{ width: '150px' }}>
 | 
				
			||||||
                Total
 | 
					                Total
 | 
				
			||||||
              </Table.Th>
 | 
					              </Table.Th>
 | 
				
			||||||
              <Table.Th ta={'center'} style={{ width: '80px' }}>
 | 
					              <Table.Th ta={'center'} style={{ width: '130px' }}>
 | 
				
			||||||
                Off
 | 
					                Off
 | 
				
			||||||
              </Table.Th>
 | 
					              </Table.Th>
 | 
				
			||||||
              <Table.Th ta={'center'} style={{ width: '80px' }}>
 | 
					              <Table.Th ta={'center'} style={{ width: '80px' }}>
 | 
				
			||||||
| 
						 | 
					@ -472,24 +567,43 @@ const LeaveManagement = () => {
 | 
				
			||||||
            {data.map((user, index) => {
 | 
					            {data.map((user, index) => {
 | 
				
			||||||
              let totalDayOff = 0
 | 
					              let totalDayOff = 0
 | 
				
			||||||
              let totalDayLeave =
 | 
					              let totalDayLeave =
 | 
				
			||||||
                user.leaveDay.ld_day + user.leaveDay.ld_date_additional
 | 
					                user.leaveDay.ld_day_total +
 | 
				
			||||||
 | 
					                user.leaveDay.ld_additional_day +
 | 
				
			||||||
 | 
					                user.leaveDay.ld_special_leave_day
 | 
				
			||||||
 | 
					              let ld_day_total = user.leaveDay.ld_day_total
 | 
				
			||||||
 | 
					              let ld_additional_day = user.leaveDay.ld_additional_day
 | 
				
			||||||
 | 
					              let ld_special_leave_day = user.leaveDay.ld_special_leave_day
 | 
				
			||||||
              let ld_note = user.leaveDay.ld_note
 | 
					              let ld_note = user.leaveDay.ld_note
 | 
				
			||||||
 | 
					              let totalOnLeave = 0
 | 
				
			||||||
 | 
					              let totalLeaveWithoutPay = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return (
 | 
					              return (
 | 
				
			||||||
                <Table.Tr key={user.user.id} className={classes.tableTr}>
 | 
					                <Table.Tr key={user.user.id} className={classes.tableTr}>
 | 
				
			||||||
                  <Table.Td ta={'center'}>{index + 1}</Table.Td>
 | 
					                  <Table.Td ta={'center'}>{index + 1}</Table.Td>
 | 
				
			||||||
                  <Table.Td>
 | 
					                  <Table.Td>
 | 
				
			||||||
                    <Tooltip multiline label={user.user.name}>
 | 
					                    <Tooltip multiline label={user.user.name}>
 | 
				
			||||||
                      <div style={{display:'flex', alignItems:'center'}}><Avatar size={'md'} mr={'md'} src={import.meta.env.VITE_BACKEND_URL.includes('local')
 | 
					                      <div style={{ display: 'flex', alignItems: 'center' }}>
 | 
				
			||||||
 | 
					                        <Avatar
 | 
				
			||||||
 | 
					                          size={'md'}
 | 
				
			||||||
 | 
					                          mr={'md'}
 | 
				
			||||||
 | 
					                          src={
 | 
				
			||||||
 | 
					                            import.meta.env.VITE_BACKEND_URL.includes('local')
 | 
				
			||||||
                          ? import.meta.env.VITE_BACKEND_URL +
 | 
					                          ? import.meta.env.VITE_BACKEND_URL +
 | 
				
			||||||
                            'storage/' +
 | 
					                            'storage/' +
 | 
				
			||||||
                           user.user.avatar
 | 
					                           user.user.avatar
 | 
				
			||||||
                          : import.meta.env.VITE_BACKEND_URL +
 | 
					                          : import.meta.env.VITE_BACKEND_URL +
 | 
				
			||||||
                            'image/storage/' +
 | 
					                            'image/storage/' +
 | 
				
			||||||
                           user.user.avatar}/>{user.user.name}</div>
 | 
					                                user.user.avatar
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        {user.user.name}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
                    </Tooltip>
 | 
					                    </Tooltip>
 | 
				
			||||||
                  </Table.Td>
 | 
					                  </Table.Td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  {monthInYear.map((d, i) => {
 | 
					                  {monthInYear.map((d, i) => {
 | 
				
			||||||
 | 
					                    // const isCurrentMonth =
 | 
				
			||||||
 | 
					                    //   Number(date.year) === currentYear && d.value === currentMonth
 | 
				
			||||||
                    let leaveDataByMonth = getDetailLeaveDay(
 | 
					                    let leaveDataByMonth = getDetailLeaveDay(
 | 
				
			||||||
                      user.monthlyLeaveDays,
 | 
					                      user.monthlyLeaveDays,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
| 
						 | 
					@ -497,6 +611,18 @@ const LeaveManagement = () => {
 | 
				
			||||||
                    const monthData = leaveDataByMonth[d.value]
 | 
					                    const monthData = leaveDataByMonth[d.value]
 | 
				
			||||||
                    let total = monthData ? monthData.leave_days : 0
 | 
					                    let total = monthData ? monthData.leave_days : 0
 | 
				
			||||||
                    totalDayOff = totalDayOff + total
 | 
					                    totalDayOff = totalDayOff + total
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    user.monthlyLeaveDays
 | 
				
			||||||
 | 
					                      .filter((item) => item.month === d.value)
 | 
				
			||||||
 | 
					                      .map((item) => {
 | 
				
			||||||
 | 
					                        if (item.reason_code === 'ONLEAVE') {
 | 
				
			||||||
 | 
					                          totalOnLeave = totalOnLeave + Number(item.leave_days)
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                          totalLeaveWithoutPay =
 | 
				
			||||||
 | 
					                            totalLeaveWithoutPay + Number(item.leave_days)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return (
 | 
					                    return (
 | 
				
			||||||
                      <Table.Td
 | 
					                      <Table.Td
 | 
				
			||||||
                        bg={total > 0 ? '#ffb5b5' : ''}
 | 
					                        bg={total > 0 ? '#ffb5b5' : ''}
 | 
				
			||||||
| 
						 | 
					@ -523,35 +649,128 @@ const LeaveManagement = () => {
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                  })}
 | 
					                  })}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  {/* Total */}
 | 
				
			||||||
                  <Table.Td
 | 
					                  <Table.Td
 | 
				
			||||||
                    ta={'center'}
 | 
					                    ta={'center'}
 | 
				
			||||||
                    bg={totalDayLeave > 0 ? '#92e6f2' : ''}
 | 
					                    // bg={totalDayLeave > 0 ? '#92e6f2' : ''}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {totalDayLeave}
 | 
					                    <p
 | 
				
			||||||
 | 
					                      style={{
 | 
				
			||||||
 | 
					                        // backgroundColor: '#c3ffc3',
 | 
				
			||||||
 | 
					                        display: ld_day_total > 0 ? 'block' : 'none',
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {'Phép năm:'}{' '}
 | 
				
			||||||
 | 
					                      <span
 | 
				
			||||||
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          backgroundColor: '#c3ffc3',
 | 
				
			||||||
 | 
					                          padding: '5px',
 | 
				
			||||||
 | 
					                          borderRadius: '5px',
 | 
				
			||||||
 | 
					                          fontWeight: 'bold',
 | 
				
			||||||
 | 
					                          color: 'black',
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        {ld_day_total}
 | 
				
			||||||
 | 
					                      </span>
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                    <p
 | 
				
			||||||
 | 
					                      style={{
 | 
				
			||||||
 | 
					                        // backgroundColor: '#92e6f2',
 | 
				
			||||||
 | 
					                        display: ld_additional_day > 0 ? 'block' : 'none',
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {'Phép năm cũ:'}{' '}
 | 
				
			||||||
 | 
					                      <span
 | 
				
			||||||
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          backgroundColor: '#92e6f2',
 | 
				
			||||||
 | 
					                          padding: '5px',
 | 
				
			||||||
 | 
					                          borderRadius: '5px',
 | 
				
			||||||
 | 
					                          fontWeight: 'bold',
 | 
				
			||||||
 | 
					                          color: 'black',
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        {ld_additional_day}
 | 
				
			||||||
 | 
					                      </span>
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                    <p
 | 
				
			||||||
 | 
					                      style={{
 | 
				
			||||||
 | 
					                        display: ld_special_leave_day > 0 ? 'block' : 'none',
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {'Phép đặc biệt:'}{' '}
 | 
				
			||||||
 | 
					                      <span
 | 
				
			||||||
 | 
					                        style={{
 | 
				
			||||||
 | 
					                          backgroundColor: '#b5cafb',
 | 
				
			||||||
 | 
					                          padding: '5px',
 | 
				
			||||||
 | 
					                          borderRadius: '5px',
 | 
				
			||||||
 | 
					                          fontWeight: 'bold',
 | 
				
			||||||
 | 
					                          color: 'black',
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        {ld_special_leave_day}
 | 
				
			||||||
 | 
					                      </span>
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
                  </Table.Td>
 | 
					                  </Table.Td>
 | 
				
			||||||
                  <Table.Td ta={'center'} bg={totalDayOff > 0 ? '#ffb5b5' : ''}>
 | 
					
 | 
				
			||||||
 | 
					                  {/* Off */}
 | 
				
			||||||
 | 
					                  <Table.Td ta={'center'}>
 | 
				
			||||||
                    {totalDayOff > 0 ? (
 | 
					                    {totalDayOff > 0 ? (
 | 
				
			||||||
                      <Tooltip
 | 
					                      <Tooltip
 | 
				
			||||||
                        multiline
 | 
					                        multiline
 | 
				
			||||||
                        label={showAllOff(user.monthlyLeaveDays)}
 | 
					                        label={showAllOff(user.monthlyLeaveDays)}
 | 
				
			||||||
                      >
 | 
					                      >
 | 
				
			||||||
                        <p> {totalDayOff}</p>
 | 
					                        <div>
 | 
				
			||||||
 | 
					                          <p
 | 
				
			||||||
 | 
					                          // style={{ backgroundColor: '#c3ffc3' }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            {'Nghỉ phép:'}{' '}
 | 
				
			||||||
 | 
					                            <span
 | 
				
			||||||
 | 
					                              style={{
 | 
				
			||||||
 | 
					                                fontWeight: 'bold',
 | 
				
			||||||
 | 
					                                color: 'black',
 | 
				
			||||||
 | 
					                                backgroundColor: '#c3ffc3',
 | 
				
			||||||
 | 
					                                padding: '5px',
 | 
				
			||||||
 | 
					                                borderRadius: '5px',
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              {totalOnLeave}
 | 
				
			||||||
 | 
					                            </span>
 | 
				
			||||||
 | 
					                          </p>
 | 
				
			||||||
 | 
					                          <p
 | 
				
			||||||
 | 
					                          //  style={{ backgroundColor: '#ffb5b5' }}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            {'Không phép:'}{' '}
 | 
				
			||||||
 | 
					                            <span
 | 
				
			||||||
 | 
					                              style={{
 | 
				
			||||||
 | 
					                                fontWeight: 'bold',
 | 
				
			||||||
 | 
					                                color: 'black',
 | 
				
			||||||
 | 
					                                backgroundColor: '#ffb5b5',
 | 
				
			||||||
 | 
					                                padding: '5px',
 | 
				
			||||||
 | 
					                                borderRadius: '5px',
 | 
				
			||||||
 | 
					                              }}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              {totalLeaveWithoutPay}
 | 
				
			||||||
 | 
					                            </span>
 | 
				
			||||||
 | 
					                          </p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
                      </Tooltip>
 | 
					                      </Tooltip>
 | 
				
			||||||
                    ) : (
 | 
					                    ) : (
 | 
				
			||||||
                      <></>
 | 
					                      <></>
 | 
				
			||||||
                    )}
 | 
					                    )}
 | 
				
			||||||
                  </Table.Td>
 | 
					                  </Table.Td>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  {/* Remaining */}
 | 
				
			||||||
                  <Table.Td
 | 
					                  <Table.Td
 | 
				
			||||||
                    ta={'center'}
 | 
					                    ta={'center'}
 | 
				
			||||||
                    bg={
 | 
					                    bg={
 | 
				
			||||||
                      totalDayLeave - totalDayOff == 0
 | 
					                      totalDayLeave - totalOnLeave == 0
 | 
				
			||||||
                        ? ''
 | 
					                        ? ''
 | 
				
			||||||
                        : totalDayLeave - totalDayOff > 0
 | 
					                        : totalDayLeave - totalOnLeave > 0
 | 
				
			||||||
                        ? '#c3ffc3'
 | 
					                        ? '#c3ffc3'
 | 
				
			||||||
                        : '#ffb5b5'
 | 
					                        : '#ffb5b5'
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {totalDayLeave - totalDayOff}
 | 
					                    {totalDayLeave - totalOnLeave}
 | 
				
			||||||
                  </Table.Td>
 | 
					                  </Table.Td>
 | 
				
			||||||
                  <Table.Td>
 | 
					                  <Table.Td>
 | 
				
			||||||
                    <Box
 | 
					                    <Box
 | 
				
			||||||
| 
						 | 
					@ -571,9 +790,12 @@ const LeaveManagement = () => {
 | 
				
			||||||
                          </Text>
 | 
					                          </Text>
 | 
				
			||||||
                        </HoverCard.Target>
 | 
					                        </HoverCard.Target>
 | 
				
			||||||
                        <HoverCard.Dropdown>
 | 
					                        <HoverCard.Dropdown>
 | 
				
			||||||
                          <Textarea size="sm" autosize>
 | 
					                          <Textarea
 | 
				
			||||||
                            {ld_note}
 | 
					                            size="sm"
 | 
				
			||||||
                          </Textarea>
 | 
					                            autosize
 | 
				
			||||||
 | 
					                            value={ld_note}
 | 
				
			||||||
 | 
					                            readOnly
 | 
				
			||||||
 | 
					                          />
 | 
				
			||||||
                        </HoverCard.Dropdown>
 | 
					                        </HoverCard.Dropdown>
 | 
				
			||||||
                      </HoverCard>
 | 
					                      </HoverCard>
 | 
				
			||||||
                    </Box>
 | 
					                    </Box>
 | 
				
			||||||
| 
						 | 
					@ -585,13 +807,17 @@ const LeaveManagement = () => {
 | 
				
			||||||
                      style={{ cursor: 'pointer' }}
 | 
					                      style={{ cursor: 'pointer' }}
 | 
				
			||||||
                      onClick={() => {
 | 
					                      onClick={() => {
 | 
				
			||||||
                        let totalLeave =
 | 
					                        let totalLeave =
 | 
				
			||||||
                          user.leaveDay.ld_day == 0
 | 
					                          user.leaveDay.ld_day_total == 0
 | 
				
			||||||
                            ? ''
 | 
					                            ? ''
 | 
				
			||||||
                            : String(user.leaveDay.ld_day)
 | 
					                            : String(user.leaveDay.ld_day_total)
 | 
				
			||||||
                        let dayAdditional =
 | 
					                        let dayAdditional =
 | 
				
			||||||
                          user.leaveDay.ld_date_additional == 0
 | 
					                          user.leaveDay.ld_additional_day == 0
 | 
				
			||||||
                            ? ''
 | 
					                            ? ''
 | 
				
			||||||
                            : String(user.leaveDay.ld_date_additional)
 | 
					                            : String(user.leaveDay.ld_additional_day)
 | 
				
			||||||
 | 
					                        let specialLeave =
 | 
				
			||||||
 | 
					                          user.leaveDay.ld_special_leave_day == 0
 | 
				
			||||||
 | 
					                            ? ''
 | 
				
			||||||
 | 
					                            : String(user.leaveDay.ld_special_leave_day)
 | 
				
			||||||
                        open1()
 | 
					                        open1()
 | 
				
			||||||
                        setCustomAddNotes({
 | 
					                        setCustomAddNotes({
 | 
				
			||||||
                          ...customAddNotes,
 | 
					                          ...customAddNotes,
 | 
				
			||||||
| 
						 | 
					@ -599,6 +825,7 @@ const LeaveManagement = () => {
 | 
				
			||||||
                          note: ld_note,
 | 
					                          note: ld_note,
 | 
				
			||||||
                          totalLeave: totalLeave,
 | 
					                          totalLeave: totalLeave,
 | 
				
			||||||
                          dayAdditional: dayAdditional,
 | 
					                          dayAdditional: dayAdditional,
 | 
				
			||||||
 | 
					                          specialLeave: specialLeave,
 | 
				
			||||||
                          user: {
 | 
					                          user: {
 | 
				
			||||||
                            id: user.user.id,
 | 
					                            id: user.user.id,
 | 
				
			||||||
                            name: user.user.name,
 | 
					                            name: user.user.name,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,16 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  deleteFile,
 | 
					  getFiles,
 | 
				
			||||||
  getAllFilesInProfiles,
 | 
					 | 
				
			||||||
  getProfilesData,
 | 
					  getProfilesData,
 | 
				
			||||||
  listUserTechnical,
 | 
					  listUserTechnical,
 | 
				
			||||||
  updateProfileFolder,
 | 
					 | 
				
			||||||
  updateProfilesData,
 | 
					  updateProfilesData,
 | 
				
			||||||
  updateUserTechnical,
 | 
					  updateUserTechnical,
 | 
				
			||||||
 | 
					  uploadFiles
 | 
				
			||||||
} from '@/api/Admin'
 | 
					} from '@/api/Admin'
 | 
				
			||||||
import { changePassword } from '@/api/Auth'
 | 
					import { changePassword } from '@/api/Auth'
 | 
				
			||||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
					import DataTableAll from '@/components/DataTable/DataTable'
 | 
				
			||||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
					import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
				
			||||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
					import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
				
			||||||
import { logout } from '@/rtk/dispatches/auth'
 | 
					import { logout } from '@/rtk/dispatches/auth'
 | 
				
			||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
					 | 
				
			||||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
					import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
					import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
				
			||||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
					import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
				
			||||||
| 
						 | 
					@ -22,16 +20,13 @@ import {
 | 
				
			||||||
  Avatar,
 | 
					  Avatar,
 | 
				
			||||||
  Box,
 | 
					  Box,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  FileInput,
 | 
					 | 
				
			||||||
  Flex,
 | 
					  Flex,
 | 
				
			||||||
  Group,
 | 
					 | 
				
			||||||
  Loader,
 | 
					  Loader,
 | 
				
			||||||
  Modal,
 | 
					  Modal,
 | 
				
			||||||
  PasswordInput,
 | 
					  PasswordInput,
 | 
				
			||||||
  Stack,
 | 
					 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Title,
 | 
					  Title
 | 
				
			||||||
} from '@mantine/core'
 | 
					} from '@mantine/core'
 | 
				
			||||||
import { notifications } from '@mantine/notifications'
 | 
					import { notifications } from '@mantine/notifications'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -46,15 +41,26 @@ import moment from 'moment'
 | 
				
			||||||
import { useCallback, useEffect, useState } from 'react'
 | 
					import { useCallback, useEffect, useState } from 'react'
 | 
				
			||||||
import { useNavigate } from 'react-router-dom'
 | 
					import { useNavigate } from 'react-router-dom'
 | 
				
			||||||
import classes from './Profile.module.css'
 | 
					import classes from './Profile.module.css'
 | 
				
			||||||
 | 
					import FileUploadForm from './components/FileUploadForm'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isCompactMenu = false
 | 
					const isCompactMenu = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TFileProfile = {
 | 
					// type TFileProfile = {
 | 
				
			||||||
  label: string
 | 
					//   label: string
 | 
				
			||||||
  type: string
 | 
					//   type: string
 | 
				
			||||||
  value: string
 | 
					//   value: string
 | 
				
			||||||
  children?: TFileProfile[]
 | 
					//   children?: TFileProfile[]
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileData {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  created_at: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Profile = () => {
 | 
					const Profile = () => {
 | 
				
			||||||
  const user = useAppSelector((state) => state.authentication.user)
 | 
					  const user = useAppSelector((state) => state.authentication.user)
 | 
				
			||||||
  const userData = getUser()
 | 
					  const userData = getUser()
 | 
				
			||||||
| 
						 | 
					@ -77,6 +83,11 @@ const Profile = () => {
 | 
				
			||||||
  const navigate = useNavigate()
 | 
					  const navigate = useNavigate()
 | 
				
			||||||
  const dispatch = useAppDispatch()
 | 
					  const dispatch = useAppDispatch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false)
 | 
				
			||||||
 | 
					  const [data, setData] = useState<FileData[]>([])
 | 
				
			||||||
 | 
					  const [openedProfile, setOpenedProfile] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateAvatar = async (file: File) => {
 | 
					  const updateAvatar = async (file: File) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await postImage(updateProfilesData, file, 'post')
 | 
					      const res = await postImage(updateProfilesData, file, 'post')
 | 
				
			||||||
| 
						 | 
					@ -137,102 +148,113 @@ const Profile = () => {
 | 
				
			||||||
    return []
 | 
					    return []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [cv, setCv] = useState<File>()
 | 
					  // const [cv, setCv] = useState<File>()
 | 
				
			||||||
  const [idCard, setIdCard] = useState<File>()
 | 
					  // const [idCard, setIdCard] = useState<File>()
 | 
				
			||||||
  const [transcript, setTranscript] = useState<File>()
 | 
					  // const [transcript, setTranscript] = useState<File>()
 | 
				
			||||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
					  // const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
				
			||||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
					  // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
				
			||||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
					  // const handleOtherFileChange = (
 | 
				
			||||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
					  //   index: number,
 | 
				
			||||||
  const handleOtherFileChange = (
 | 
					  //   field: string,
 | 
				
			||||||
    index: number,
 | 
					  //   value: File | string,
 | 
				
			||||||
    field: string,
 | 
					  // ) => {
 | 
				
			||||||
    value: File | string,
 | 
					  //   const updatedFiles: any = [...otherFiles]
 | 
				
			||||||
 | 
					  //   updatedFiles[index][field] = value
 | 
				
			||||||
 | 
					  //   setOtherFiles(updatedFiles)
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // const addOtherFileInput = () => {
 | 
				
			||||||
 | 
					  //   setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = async (
 | 
				
			||||||
 | 
					    e: React.FormEvent,
 | 
				
			||||||
 | 
					    fileName: string,
 | 
				
			||||||
 | 
					    description: string,
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    const updatedFiles: any = [...otherFiles]
 | 
					 | 
				
			||||||
    updatedFiles[index][field] = value
 | 
					 | 
				
			||||||
    setOtherFiles(updatedFiles)
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const addOtherFileInput = () => {
 | 
					 | 
				
			||||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleSubmit = async (e: any) => {
 | 
					 | 
				
			||||||
    e.preventDefault()
 | 
					    e.preventDefault()
 | 
				
			||||||
 | 
					    setIsLoading(true)
 | 
				
			||||||
    const formData = new FormData()
 | 
					    const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Append each selected file to FormData
 | 
					    if (selectedFile) {
 | 
				
			||||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
					      formData.append('file', selectedFile)
 | 
				
			||||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
					      formData.append('name', fileName)
 | 
				
			||||||
        formData.append(
 | 
					      formData.append('description', description)
 | 
				
			||||||
          'files[]',
 | 
					      formData.append('user_name', user.user.name)
 | 
				
			||||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
					      formData.append('user_id', user.user.id.toString())
 | 
				
			||||||
        )
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const token = await getAccessToken()
 | 
				
			||||||
 | 
					        const response = await axios.post(uploadFiles, formData, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					            Authorization: `Bearer ${token}`,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (response.status === 200) {
 | 
				
			||||||
 | 
					          await getAllFile()
 | 
				
			||||||
 | 
					          setSelectedFile(null)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error uploading file:', error)
 | 
				
			||||||
 | 
					      } finally {
 | 
				
			||||||
 | 
					        setIsLoading(false)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (cv) {
 | 
					 | 
				
			||||||
      formData.append('files[]', cv)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (idCard) {
 | 
					 | 
				
			||||||
      formData.append('files[]', idCard)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (transcript) {
 | 
					 | 
				
			||||||
      formData.append('files[]', transcript)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (universityDiploma) {
 | 
					 | 
				
			||||||
      formData.append('files[]', universityDiploma)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const token = await getAccessToken()
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
					 | 
				
			||||||
        headers: {
 | 
					 | 
				
			||||||
          'Content-Type': 'multipart/form-data',
 | 
					 | 
				
			||||||
          Authorization: `Bearer ${token}`,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      if(response.status === 200){
 | 
					 | 
				
			||||||
        getAllFile()
 | 
					 | 
				
			||||||
        setOtherFiles([])
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
      console.error('Error uploading files', error)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getAllFile = async () => {
 | 
					  const getAllFile = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await get(getAllFilesInProfiles, {
 | 
					      const res = await get(getFiles)
 | 
				
			||||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      if (res.status === true) {
 | 
					      if (res.status === true) {
 | 
				
			||||||
        setData(res.data)
 | 
					        const userFiles = res.data[user.user.name] || [];
 | 
				
			||||||
 | 
					        setData(userFiles);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(error)
 | 
					      console.log(error)
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Lỗi',
 | 
				
			||||||
 | 
					        message: 'Không thể tải danh sách file',
 | 
				
			||||||
 | 
					        color: 'red',
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const removeFile = async (url: string) => {
 | 
					  const removeFile = async (id: number) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await Xdelete(deleteFile, {file_url: url}, getAllFile)
 | 
					      const token = await getAccessToken();
 | 
				
			||||||
 | 
					      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          Authorization: `Bearer ${token}`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (response.status === 200) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: 'Thành công',
 | 
				
			||||||
 | 
					          message: 'Xóa file thành công',
 | 
				
			||||||
 | 
					          color: 'green',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await getAllFile();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(error)
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Lỗi',
 | 
				
			||||||
 | 
					        message: 'Không thể xóa file',
 | 
				
			||||||
 | 
					        color: 'red',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const fetchData = async () => {
 | 
					    const fetchData = async () => {
 | 
				
			||||||
      const result = await getListProfilesData()
 | 
					      const result = await getListProfilesData()
 | 
				
			||||||
      setDataProfile(result ?? [])
 | 
					      setDataProfile(result ?? [])
 | 
				
			||||||
 | 
					      await getAllFile()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    fetchData()
 | 
					    fetchData()
 | 
				
			||||||
    getAllFile()
 | 
					 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleChangePassword = async () => {
 | 
					  const handleChangePassword = async () => {
 | 
				
			||||||
| 
						 | 
					@ -306,23 +328,19 @@ const Profile = () => {
 | 
				
			||||||
    dispatch(logout(navigate))
 | 
					    dispatch(logout(navigate))
 | 
				
			||||||
  }, [dispatch, navigate])
 | 
					  }, [dispatch, navigate])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
					  // const handleChangeFileName = (e: File, newName: string): File => {
 | 
				
			||||||
    const originalFile = e // Get the original file
 | 
					  //   const originalFile = e;
 | 
				
			||||||
    const extend = originalFile.name.split('.')[1]
 | 
					  //   const extend = originalFile.name.split('.')[1];
 | 
				
			||||||
    if (originalFile) {
 | 
					  //   const newFileName = `${newName}.${extend}`;
 | 
				
			||||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
					  //   return new File([originalFile], newFileName, {
 | 
				
			||||||
      const newFile = new File([originalFile], newFileName, {
 | 
					  //     type: originalFile.type,
 | 
				
			||||||
        type: originalFile.type,
 | 
					  //   });
 | 
				
			||||||
      }) // Create new file object
 | 
					  // };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return newFile // Save the new file object for further processing
 | 
					  // const checkFileExist = (nameField: string) => {
 | 
				
			||||||
    }
 | 
					  //   const file = data.find((f) => f.name.includes(nameField));
 | 
				
			||||||
  }
 | 
					  //   return file;
 | 
				
			||||||
 | 
					  // };
 | 
				
			||||||
  const checkFileExist = (nameField: string) => {
 | 
					 | 
				
			||||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
					 | 
				
			||||||
    return file
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
| 
						 | 
					@ -578,162 +596,14 @@ const Profile = () => {
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Box>
 | 
					          <Box>
 | 
				
			||||||
            <form onSubmit={handleSubmit}>
 | 
					            <FileUploadForm
 | 
				
			||||||
              <Stack>
 | 
					              data={data}
 | 
				
			||||||
                <Box
 | 
					              handleSubmit={handleSubmit}
 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
					              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
				
			||||||
                >
 | 
					              removeFile={removeFile}
 | 
				
			||||||
                  <Text>CV</Text>
 | 
					              isLoading={isLoading}
 | 
				
			||||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
					              currentUser={user.user.name}
 | 
				
			||||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
					            />
 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('cv')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label={'CV'}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CV"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    0
 | 
					 | 
				
			||||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.doc,.docx"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>CCCD</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('idCard')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="CCCD"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('idCard') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CCCD"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bảng điểm</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('transcript')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bảng điểm"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('transcript') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bảng điểm"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'flex'
 | 
					 | 
				
			||||||
                      : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bằng đại học</Text>
 | 
					 | 
				
			||||||
                  <a
 | 
					 | 
				
			||||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <Text>{`: ${
 | 
					 | 
				
			||||||
                      checkFileExist('universityDiploma')?.label
 | 
					 | 
				
			||||||
                    }`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('universityDiploma')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bằng đại học"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('universityDiploma') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bằng đại học"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setUniversityDiploma(
 | 
					 | 
				
			||||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Text>Danh sách file khác:</Text>
 | 
					 | 
				
			||||||
                  <Box>
 | 
					 | 
				
			||||||
                    {data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{
 | 
					 | 
				
			||||||
                      return <Box key={index}
 | 
					 | 
				
			||||||
                      style={{
 | 
					 | 
				
			||||||
                        display: 'flex',
 | 
					 | 
				
			||||||
                        marginBottom:'5px'
 | 
					 | 
				
			||||||
                      }}
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <a
 | 
					 | 
				
			||||||
                        href={c?.value}
 | 
					 | 
				
			||||||
                        target="_blank"
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                        <Text>{`${
 | 
					 | 
				
			||||||
                          c?.label
 | 
					 | 
				
			||||||
                        }`}</Text>
 | 
					 | 
				
			||||||
                      </a>
 | 
					 | 
				
			||||||
                      <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(c?.value!)}}>
 | 
					 | 
				
			||||||
                        Delete
 | 
					 | 
				
			||||||
                      </Button>
 | 
					 | 
				
			||||||
                    </Box>
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
                  </Box>
 | 
					 | 
				
			||||||
                {otherFiles.map((fileInput, index) => (
 | 
					 | 
				
			||||||
                  <Group key={index}>
 | 
					 | 
				
			||||||
                    <FileInput
 | 
					 | 
				
			||||||
                      placeholder="Chọn file"
 | 
					 | 
				
			||||||
                      onChange={(file) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(index, 'file', file!)
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      w={'30%'}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                    <TextInput
 | 
					 | 
				
			||||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
					 | 
				
			||||||
                      value={fileInput.type}
 | 
					 | 
				
			||||||
                      w={'65%'}
 | 
					 | 
				
			||||||
                      onChange={(e) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(
 | 
					 | 
				
			||||||
                          index,
 | 
					 | 
				
			||||||
                          'type',
 | 
					 | 
				
			||||||
                          e.currentTarget.value,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </Group>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
					 | 
				
			||||||
                  Thêm file khác
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Button type="submit" color="blue">
 | 
					 | 
				
			||||||
                  Save
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
              </Stack>
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Modal>
 | 
					        </Modal>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,121 @@
 | 
				
			||||||
 | 
					.fileContainer {
 | 
				
			||||||
 | 
					    padding: 12px;
 | 
				
			||||||
 | 
					    margin: 8px 0;
 | 
				
			||||||
 | 
					    border: 1px solid #e9ecef;
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    transition: all 0.2s ease;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileContainer:hover {
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileHeader {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileName {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileDescription {
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  margin: 2px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileActions {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 8px;
 | 
				
			||||||
 | 
					  margin-top: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileLink {
 | 
				
			||||||
 | 
					  color: #228be6;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  transition: color 0.2s ease;
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  background-color: #e7f5ff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileLink:hover {
 | 
				
			||||||
 | 
					  color: #1c7ed6;
 | 
				
			||||||
 | 
					  background-color: #d0ebff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.deleteButton {
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileInputGroup {
 | 
				
			||||||
 | 
					  padding: 16px;
 | 
				
			||||||
 | 
					  margin: 16px 0;
 | 
				
			||||||
 | 
					  border: 2px dashed #e9ecef;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.fileInput,
 | 
				
			||||||
 | 
					.fileNameInput,
 | 
				
			||||||
 | 
					.descriptionInput {
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.saveButton {
 | 
				
			||||||
 | 
					  margin-top: 16px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  max-width: 180px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.saveButton:disabled {
 | 
				
			||||||
 | 
					  background-color: #e9ecef;
 | 
				
			||||||
 | 
					  cursor: not-allowed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loadingOverlay {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  background-color: rgba(255, 255, 255, 0.8);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.loadingSpinner {
 | 
				
			||||||
 | 
					  width: 32px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					  border: 3px solid #f3f3f3;
 | 
				
			||||||
 | 
					  border-top: 3px solid #3498db;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  animation: spin 1s linear infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sectionTitle {
 | 
				
			||||||
 | 
					  font-size: 16px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					  color: #343a40;
 | 
				
			||||||
 | 
					  padding-bottom: 8px;
 | 
				
			||||||
 | 
					  border-bottom: 1px solid #e9ecef;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes spin {
 | 
				
			||||||
 | 
					  0% { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					  100% { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,251 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Card,
 | 
				
			||||||
 | 
					  FileInput,
 | 
				
			||||||
 | 
					  Group,
 | 
				
			||||||
 | 
					  Stack,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					  Textarea,
 | 
				
			||||||
 | 
					} from '@mantine/core'
 | 
				
			||||||
 | 
					import { notifications } from '@mantine/notifications'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IconDownload,
 | 
				
			||||||
 | 
					  IconFileTypeDocx,
 | 
				
			||||||
 | 
					  IconFileTypePdf,
 | 
				
			||||||
 | 
					  IconFileTypeXls,
 | 
				
			||||||
 | 
					  IconPhoto,
 | 
				
			||||||
 | 
					  IconSearch,
 | 
				
			||||||
 | 
					  IconTrash,
 | 
				
			||||||
 | 
					} from '@tabler/icons-react'
 | 
				
			||||||
 | 
					import { useState } from 'react'
 | 
				
			||||||
 | 
					import classes from './FileUploadForm.module.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// type TFileProfile = {
 | 
				
			||||||
 | 
					//   label: string
 | 
				
			||||||
 | 
					//   type: string
 | 
				
			||||||
 | 
					//   value: string
 | 
				
			||||||
 | 
					//   children?: TFileProfile[]
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileData {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  url: string
 | 
				
			||||||
 | 
					  type: string
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  created_at: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FileUploadFormProps = {
 | 
				
			||||||
 | 
					  data: FileData[];
 | 
				
			||||||
 | 
					  handleSubmit: (e: React.FormEvent, fileName: string, description: string, currentUser: string) => Promise<boolean | void>;
 | 
				
			||||||
 | 
					  handleFileChange: (file: File | null) => void;
 | 
				
			||||||
 | 
					  removeFile: (id: number) => Promise<void>;
 | 
				
			||||||
 | 
					  isLoading: boolean;
 | 
				
			||||||
 | 
					  currentUser: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FileUploadForm = ({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  handleSubmit,
 | 
				
			||||||
 | 
					  handleFileChange,
 | 
				
			||||||
 | 
					  removeFile,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  currentUser,
 | 
				
			||||||
 | 
					}: FileUploadFormProps) => {
 | 
				
			||||||
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
				
			||||||
 | 
					  const [fileName, setFileName] = useState('')
 | 
				
			||||||
 | 
					  const [description, setDescription] = useState('')
 | 
				
			||||||
 | 
					  const [isUploading, setIsUploading] = useState(false)
 | 
				
			||||||
 | 
					  const [searchTerm, setSearchTerm] = useState('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleFileSelect = (file: File | null) => {
 | 
				
			||||||
 | 
					    setSelectedFile(file)
 | 
				
			||||||
 | 
					    handleFileChange(file)
 | 
				
			||||||
 | 
					    if (file) {
 | 
				
			||||||
 | 
					      // Set default name as file name without extension
 | 
				
			||||||
 | 
					      setFileName(file.name.split('.')[0])
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleFormSubmit = async (e: React.FormEvent) => {
 | 
				
			||||||
 | 
					    e.preventDefault()
 | 
				
			||||||
 | 
					    setIsUploading(true)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await handleSubmit(e, fileName, description, currentUser)
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Thành công',
 | 
				
			||||||
 | 
					        message: 'Tải file lên thành công',
 | 
				
			||||||
 | 
					        color: 'green',
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      setFileName('')
 | 
				
			||||||
 | 
					      setDescription('')
 | 
				
			||||||
 | 
					      setSelectedFile(null)
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('Error submitting form:', error)
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Lỗi',
 | 
				
			||||||
 | 
					        message: 'Không thể tải file lên',
 | 
				
			||||||
 | 
					        color: 'red',
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setIsUploading(false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getFileIcon = (type: string) => {
 | 
				
			||||||
 | 
					    switch (type) {
 | 
				
			||||||
 | 
					      case 'document':
 | 
				
			||||||
 | 
					        return <IconFileTypeDocx size={16} />
 | 
				
			||||||
 | 
					      case 'image':
 | 
				
			||||||
 | 
					        return <IconPhoto size={16} />
 | 
				
			||||||
 | 
					      case 'spreadsheet':
 | 
				
			||||||
 | 
					        return <IconFileTypeXls size={16} />
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return <IconFileTypePdf size={16} />
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const filteredFiles = data.filter(
 | 
				
			||||||
 | 
					    (file) =>
 | 
				
			||||||
 | 
					      file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
				
			||||||
 | 
					      (file.description &&
 | 
				
			||||||
 | 
					        file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {isLoading && (
 | 
				
			||||||
 | 
					        <div className={classes.loadingOverlay}>
 | 
				
			||||||
 | 
					          <div className={classes.loadingSpinner} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <form onSubmit={handleFormSubmit}>
 | 
				
			||||||
 | 
					        <Box>
 | 
				
			||||||
 | 
					          <Text className={classes.sectionTitle}>Tài liệu</Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Box className={classes.fileInputGroup}>
 | 
				
			||||||
 | 
					            <FileInput
 | 
				
			||||||
 | 
					              label="Chọn file"
 | 
				
			||||||
 | 
					              placeholder="Chọn file để tải lên"
 | 
				
			||||||
 | 
					              accept="image/png,image/jpeg,image/jpg,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet"
 | 
				
			||||||
 | 
					              onChange={handleFileSelect}
 | 
				
			||||||
 | 
					              value={selectedFile}
 | 
				
			||||||
 | 
					              className={classes.fileInput}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <TextInput
 | 
				
			||||||
 | 
					              label="Tên file"
 | 
				
			||||||
 | 
					              placeholder="Nhập tên file"
 | 
				
			||||||
 | 
					              value={fileName}
 | 
				
			||||||
 | 
					              onChange={(e) => setFileName(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.fileNameInput}
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Textarea
 | 
				
			||||||
 | 
					              label="Mô tả"
 | 
				
			||||||
 | 
					              placeholder="Nhập mô tả cho file"
 | 
				
			||||||
 | 
					              value={description}
 | 
				
			||||||
 | 
					              onChange={(e) => setDescription(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.descriptionInput}
 | 
				
			||||||
 | 
					              minRows={3}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              type="submit"
 | 
				
			||||||
 | 
					              color="blue"
 | 
				
			||||||
 | 
					              className={classes.saveButton}
 | 
				
			||||||
 | 
					              disabled={isLoading || isUploading || !selectedFile || !fileName}
 | 
				
			||||||
 | 
					              loading={isLoading || isUploading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isLoading || isUploading ? 'Đang xử lý...' : 'Lưu thay đổi'}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Box className={classes.fileListContainer}>
 | 
				
			||||||
 | 
					            <TextInput
 | 
				
			||||||
 | 
					              placeholder="Tìm kiếm theo tên hoặc mô tả..."
 | 
				
			||||||
 | 
					              leftSection={<IconSearch size={14} />}
 | 
				
			||||||
 | 
					              value={searchTerm}
 | 
				
			||||||
 | 
					              onChange={(e) => setSearchTerm(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.searchInput}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Stack className={classes.fileList} gap="xs">
 | 
				
			||||||
 | 
					              {filteredFiles.map((file) => (
 | 
				
			||||||
 | 
					                <Card
 | 
				
			||||||
 | 
					                  key={file.id}
 | 
				
			||||||
 | 
					                  shadow="xs"
 | 
				
			||||||
 | 
					                  padding="xs"
 | 
				
			||||||
 | 
					                  radius="sm"
 | 
				
			||||||
 | 
					                  withBorder
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Group justify="space-between" gap="xs" wrap="nowrap">
 | 
				
			||||||
 | 
					                    <Group gap="xs" className={classes.cardContent}>
 | 
				
			||||||
 | 
					                      {getFileIcon(file.type)}
 | 
				
			||||||
 | 
					                      <Box style={{ minWidth: 0 }}>
 | 
				
			||||||
 | 
					                        <Text size="xs" fw={500} className={classes.cardTitle}>
 | 
				
			||||||
 | 
					                          {file.name}
 | 
				
			||||||
 | 
					                        </Text>
 | 
				
			||||||
 | 
					                        {file.description && (
 | 
				
			||||||
 | 
					                          <Text
 | 
				
			||||||
 | 
					                            size="xs"
 | 
				
			||||||
 | 
					                            c="dimmed"
 | 
				
			||||||
 | 
					                            className={classes.cardDescription}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            {file.description}
 | 
				
			||||||
 | 
					                          </Text>
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                        <Text size="xs" c="dimmed">
 | 
				
			||||||
 | 
					                          Uploaded:{' '}
 | 
				
			||||||
 | 
					                          {new Date(file.created_at).toLocaleDateString()}
 | 
				
			||||||
 | 
					                        </Text>
 | 
				
			||||||
 | 
					                      </Box>
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                    <Group gap="xs" wrap="nowrap">
 | 
				
			||||||
 | 
					                      <Button
 | 
				
			||||||
 | 
					                        size="xs"
 | 
				
			||||||
 | 
					                        variant="light"
 | 
				
			||||||
 | 
					                        color="blue"
 | 
				
			||||||
 | 
					                        component="a"
 | 
				
			||||||
 | 
					                        href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
				
			||||||
 | 
					                          import.meta.env.VITE_BACKEND_URL?.includes(
 | 
				
			||||||
 | 
					                            'localhost',
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
 | 
					                            ? ''
 | 
				
			||||||
 | 
					                            : 'image/'
 | 
				
			||||||
 | 
					                        }${file.url.slice(1)}`}
 | 
				
			||||||
 | 
					                        target="_blank"
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Group gap={2}>
 | 
				
			||||||
 | 
					                          <IconDownload size={12} />
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                      <Button
 | 
				
			||||||
 | 
					                        size="xs"
 | 
				
			||||||
 | 
					                        variant="light"
 | 
				
			||||||
 | 
					                        color="red"
 | 
				
			||||||
 | 
					                        onClick={() => removeFile(file.id)}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Group gap={2}>
 | 
				
			||||||
 | 
					                          <IconTrash size={12} />
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                  </Group>
 | 
				
			||||||
 | 
					                </Card>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Stack>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FileUploadForm
 | 
				
			||||||
| 
						 | 
					@ -5,8 +5,8 @@ import {
 | 
				
			||||||
  getTicketsOfUser,
 | 
					  getTicketsOfUser,
 | 
				
			||||||
} from '@/api/Admin'
 | 
					} from '@/api/Admin'
 | 
				
			||||||
import { DataTablePagination } from '@/components/DataTable/DataTable'
 | 
					import { DataTablePagination } from '@/components/DataTable/DataTable'
 | 
				
			||||||
import { Xdelete, create } from '@/rtk/helpers/CRUD'
 | 
					import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
				
			||||||
import { get } from '@/rtk/helpers/apiService'
 | 
					import { get, post } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Badge,
 | 
					  Badge,
 | 
				
			||||||
  Box,
 | 
					  Box,
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ import { IconTrash } from '@tabler/icons-react'
 | 
				
			||||||
import moment from 'moment'
 | 
					import moment from 'moment'
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import { useEffect, useState } from 'react'
 | 
				
			||||||
import classes from './Tickets.module.css'
 | 
					import classes from './Tickets.module.css'
 | 
				
			||||||
 | 
					import { _NOTIFICATION_MESS } from '@/rtk/helpers/notificationMess'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TTickets = {
 | 
					type TTickets = {
 | 
				
			||||||
  id: number
 | 
					  id: number
 | 
				
			||||||
| 
						 | 
					@ -82,6 +83,11 @@ const Tickets = () => {
 | 
				
			||||||
  const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
 | 
					  const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
 | 
				
			||||||
  const [dataReason, setDataReason] = useState<DataReason[]>([])
 | 
					  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) => {
 | 
					  const getListMasterByType = async (type: string) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const params = {
 | 
					      const params = {
 | 
				
			||||||
| 
						 | 
					@ -271,27 +277,61 @@ const Tickets = () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleCreate = async (values: TTickets) => {
 | 
					  const handleCreate = async (values: TTickets, confirm: boolean = false) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await create(
 | 
					      let params = {
 | 
				
			||||||
        addTicket,
 | 
					        // time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
 | 
				
			||||||
        {
 | 
					        start_date: moment(values.start_date).format('YYYY-MM-DD'),
 | 
				
			||||||
          // time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
 | 
					        start_period: values.start_period,
 | 
				
			||||||
          start_date: moment(values.start_date).format('YYYY-MM-DD'),
 | 
					        end_date: moment(values.end_date).format('YYYY-MM-DD'),
 | 
				
			||||||
          start_period: values.start_period,
 | 
					        end_period: values.end_period,
 | 
				
			||||||
          end_date: moment(values.end_date).format('YYYY-MM-DD'),
 | 
					        type: values.type,
 | 
				
			||||||
          end_period: values.end_period,
 | 
					        reason: values.reason,
 | 
				
			||||||
          type: values.type,
 | 
					        is_accept: confirm,
 | 
				
			||||||
          reason: values.reason,
 | 
					      }
 | 
				
			||||||
        },
 | 
					
 | 
				
			||||||
        getAllTickets,
 | 
					      let res = await post(addTicket, params)
 | 
				
			||||||
      )
 | 
					
 | 
				
			||||||
      if (res === true) {
 | 
					      if (res.status) {
 | 
				
			||||||
 | 
					        notifications.show({
 | 
				
			||||||
 | 
					          title: 'Success',
 | 
				
			||||||
 | 
					          message: _NOTIFICATION_MESS.create_success,
 | 
				
			||||||
 | 
					          color: 'green',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        setAction('')
 | 
					        setAction('')
 | 
				
			||||||
        form.reset()
 | 
					        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,9 +362,7 @@ const Tickets = () => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <div className={classes.title}>
 | 
					      <div className={classes.title}>
 | 
				
			||||||
        <h3>
 | 
					        <h3>Tickets</h3>
 | 
				
			||||||
          Tickets
 | 
					 | 
				
			||||||
        </h3>
 | 
					 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          m={5}
 | 
					          m={5}
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
| 
						 | 
					@ -490,6 +528,56 @@ const Tickets = () => {
 | 
				
			||||||
          </Group>
 | 
					          </Group>
 | 
				
			||||||
        </Text>
 | 
					        </Text>
 | 
				
			||||||
      </Dialog>
 | 
					      </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>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,3 +60,37 @@
 | 
				
			||||||
  padding-top: 5px;
 | 
					  padding-top: 5px;
 | 
				
			||||||
  padding-bottom: 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,9 @@ const Timekeeping = () => {
 | 
				
			||||||
  const [exportModalOpened, setExportModalOpened] = useState(false)
 | 
					  const [exportModalOpened, setExportModalOpened] = useState(false)
 | 
				
			||||||
  const [exportOption, setExportOption] = useState('default')
 | 
					  const [exportOption, setExportOption] = useState('default')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false)
 | 
				
			||||||
 | 
					  const [noteToDelete, setNoteToDelete] = useState<any>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getListMasterByType = async (type: string) => {
 | 
					  const getListMasterByType = async (type: string) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const params = {
 | 
					      const params = {
 | 
				
			||||||
| 
						 | 
					@ -452,9 +455,9 @@ const Timekeeping = () => {
 | 
				
			||||||
          month: date.month,
 | 
					          month: date.month,
 | 
				
			||||||
          year: date.year,
 | 
					          year: date.year,
 | 
				
			||||||
          working_days: workingDays,
 | 
					          working_days: workingDays,
 | 
				
			||||||
          option: option
 | 
					          option: option,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        fileName
 | 
					        fileName,
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      setExportModalOpened(false)
 | 
					      setExportModalOpened(false)
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
| 
						 | 
					@ -462,6 +465,53 @@ const Timekeeping = () => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleConfirmDelete = async () => {
 | 
				
			||||||
 | 
					    if (noteToDelete) {
 | 
				
			||||||
 | 
					      await handleDelete(noteToDelete.id)
 | 
				
			||||||
 | 
					      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}>
 | 
				
			||||||
 | 
					        Are you sure you want to delete this note?
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Box className={classes.deleteModalFooter}>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="outline"
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            setIsDeleteConfirmOpen(false)
 | 
				
			||||||
 | 
					            setNoteToDelete(null)
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Cancel
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button className={classes.deleteButton} onClick={handleConfirmDelete}>
 | 
				
			||||||
 | 
					          Delete
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <div className={classes.title}>
 | 
					      <div className={classes.title}>
 | 
				
			||||||
| 
						 | 
					@ -631,11 +681,7 @@ const Timekeeping = () => {
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <IconTrash
 | 
					                <IconTrash
 | 
				
			||||||
                  className={classes.deleteIcon}
 | 
					                  className={classes.deleteIcon}
 | 
				
			||||||
                  onClick={async () => {
 | 
					                  onClick={() => openDeleteConfirm(item)}
 | 
				
			||||||
                    await handleDelete(item.id)
 | 
					 | 
				
			||||||
                    // handleUpdateCacheMonth()
 | 
					 | 
				
			||||||
                    // close2()
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  width={20}
 | 
					                  width={20}
 | 
				
			||||||
                  height={20}
 | 
					                  height={20}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
| 
						 | 
					@ -655,7 +701,7 @@ const Timekeeping = () => {
 | 
				
			||||||
              data={Array.from({ length: 12 }, (_, index) => {
 | 
					              data={Array.from({ length: 12 }, (_, index) => {
 | 
				
			||||||
                return {
 | 
					                return {
 | 
				
			||||||
                  value: (1 + index).toString(),
 | 
					                  value: (1 + index).toString(),
 | 
				
			||||||
                  label: (1 + index).toString()
 | 
					                  label: (1 + index).toString(),
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              })}
 | 
					              })}
 | 
				
			||||||
              onChange={(e) => {
 | 
					              onChange={(e) => {
 | 
				
			||||||
| 
						 | 
					@ -1074,20 +1120,19 @@ const Timekeeping = () => {
 | 
				
			||||||
          placeholder="Choose an option"
 | 
					          placeholder="Choose an option"
 | 
				
			||||||
          value={exportOption}
 | 
					          value={exportOption}
 | 
				
			||||||
          onChange={(value) => setExportOption(value || 'default')}
 | 
					          onChange={(value) => setExportOption(value || 'default')}
 | 
				
			||||||
          data={[
 | 
					          data={[{ value: 'default', label: 'Default' }]}
 | 
				
			||||||
            { value: 'default', label: 'Default' }
 | 
					 | 
				
			||||||
          ]}
 | 
					 | 
				
			||||||
          mb="md"
 | 
					          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)}>
 | 
					          <Button variant="outline" onClick={() => setExportModalOpened(false)}>
 | 
				
			||||||
            Close
 | 
					            Close
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
          <Button onClick={() => handleExport(exportOption)}>
 | 
					          <Button onClick={() => handleExport(exportOption)}>Export</Button>
 | 
				
			||||||
            Export
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Modal>
 | 
					      </Modal>
 | 
				
			||||||
 | 
					      <DeleteConfirmModal />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ export const create = async (
 | 
				
			||||||
    if (res.status === false) {
 | 
					    if (res.status === false) {
 | 
				
			||||||
      notifications.show({
 | 
					      notifications.show({
 | 
				
			||||||
        title: 'Error',
 | 
					        title: 'Error',
 | 
				
			||||||
        message: res.message ?? _NOTIFICATION_MESS.create_error,
 | 
					        message: <div style={{ whiteSpace: 'pre-line' }}>{res.message ?? _NOTIFICATION_MESS.create_error}</div>,
 | 
				
			||||||
        color: 'red',
 | 
					        color: 'red',
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue