truong-report-excel #98
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -3,7 +3,9 @@
 | 
			
		|||
namespace Modules\Admin\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Mail\WarningLongTask;
 | 
			
		||||
use App\Mail\WorklogReport;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use App\Traits\HasFilterRequest;
 | 
			
		||||
use App\Traits\HasOrderByRequest;
 | 
			
		||||
use App\Traits\HasSearchRequest;
 | 
			
		||||
| 
						 | 
				
			
			@ -183,8 +185,8 @@ class JiraController extends Controller
 | 
			
		|||
        $workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
 | 
			
		||||
 | 
			
		||||
        $tasksByUser = $this->formatWorkLogsByUser($workLogs);
 | 
			
		||||
        Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
        // Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
        // Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
        Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
 | 
			
		||||
        // return "Email sent successfully!";
 | 
			
		||||
        return response()->json([
 | 
			
		||||
| 
						 | 
				
			
			@ -248,7 +250,6 @@ class JiraController extends Controller
 | 
			
		|||
        return $tasksByUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function getAllUserDoing(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
| 
						 | 
				
			
			@ -303,6 +304,7 @@ class JiraController extends Controller
 | 
			
		|||
            'status' => true
 | 
			
		||||
        ], 200);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAllIssueByIdSprint(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $id = $request->input('id');
 | 
			
		||||
| 
						 | 
				
			
			@ -313,4 +315,26 @@ class JiraController extends Controller
 | 
			
		|||
            'status' => true
 | 
			
		||||
        ], 200);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function sendWarningMailByAllowcation()
 | 
			
		||||
    {
 | 
			
		||||
        $data = $this->jiraService->getAllUserDoing();
 | 
			
		||||
        $user_info = [];
 | 
			
		||||
        foreach ($data['projects'] as $project) {
 | 
			
		||||
            foreach ($project['users'] as $user) {
 | 
			
		||||
                foreach ($user['issues'] as $issue) {
 | 
			
		||||
                    $targetDate = Carbon::parse($issue['changelog']['histories'][0]['created']); // Target date
 | 
			
		||||
                    $daysRemaining = Carbon::now()->setTimezone(env('TIME_ZONE'))->diffInDays($targetDate);
 | 
			
		||||
                    if ($daysRemaining > 10) {
 | 
			
		||||
                        $issue['daysRemaining'] = $daysRemaining;
 | 
			
		||||
                        $user_info[$user['user']['emailAddress']][] = $issue;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($user_info as $email => $user) {
 | 
			
		||||
            Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email]));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
namespace Modules\Admin\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Exports\LeaveManagementExport;
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Jobs\InitializeLeaveDays;
 | 
			
		||||
use App\Models\LeaveDays;
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +10,7 @@ use App\Models\Notes;
 | 
			
		|||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Facades\Validator;
 | 
			
		||||
use Maatwebsite\Excel\Facades\Excel;
 | 
			
		||||
 | 
			
		||||
class LeaveManagementController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -139,4 +141,20 @@ class LeaveManagementController extends Controller
 | 
			
		|||
 | 
			
		||||
        return response()->json(['status' => true, 'message' => 'Updated successfully']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function export(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $year = $request->query('year', now()->year);
 | 
			
		||||
        $leaveDays = $this->getDataByYear($year);
 | 
			
		||||
 | 
			
		||||
        if ($leaveDays->isEmpty()) {
 | 
			
		||||
            return response()->json(['status' => false, 'message' => 'No data found']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $currentDate = date('d_His');
 | 
			
		||||
        return Excel::download(
 | 
			
		||||
            new LeaveManagementExport($leaveDays),
 | 
			
		||||
            "LeaveManagement_{$year}_{$currentDate}.xlsx"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,4 +115,126 @@ class ProfileController extends Controller
 | 
			
		|||
        $user->save();
 | 
			
		||||
        return AbstractController::ResultSuccess($path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function listFiles(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        // Get the root folder from the input URL
 | 
			
		||||
        $rootFolder = $request->input('root_folder');
 | 
			
		||||
 | 
			
		||||
        // Ensure the root folder is correctly formatted
 | 
			
		||||
        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
			
		||||
 | 
			
		||||
        // Get all files and directories in the specified root folder
 | 
			
		||||
        $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder);
 | 
			
		||||
 | 
			
		||||
        return response()->json(['data' => $fileList, 'status' => true]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getDirectoryTree($dir, $urlRoot)
 | 
			
		||||
    {
 | 
			
		||||
        $results = [];
 | 
			
		||||
 | 
			
		||||
        // Scan the directory for files and folders
 | 
			
		||||
        $files = scandir($dir);
 | 
			
		||||
 | 
			
		||||
        foreach ($files as $file) {
 | 
			
		||||
            if ($file !== '.' && $file !== '..') {
 | 
			
		||||
                $filePath = $dir . DIRECTORY_SEPARATOR . $file;
 | 
			
		||||
                $fileUrl = url($urlRoot . $file);
 | 
			
		||||
 | 
			
		||||
                if (is_dir($filePath)) {
 | 
			
		||||
                    // If it's a directory, recurse into it
 | 
			
		||||
                    $results[] = [
 | 
			
		||||
                        'label' => $file,
 | 
			
		||||
                        'type' => 'directory',
 | 
			
		||||
                        'value' => $fileUrl,
 | 
			
		||||
                        'children' => $this->getDirectoryTree($filePath, $urlRoot . $file . '/')
 | 
			
		||||
                    ];
 | 
			
		||||
                } else {
 | 
			
		||||
                    // If it's a file, add it to the list
 | 
			
		||||
                    $results[] = [
 | 
			
		||||
                        'label' => $file,
 | 
			
		||||
                        'type' => 'file',
 | 
			
		||||
                        'value' => $fileUrl
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $results;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function updateProfile(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $name = $request->input('name') ?? auth('admins')->user()->name;
 | 
			
		||||
        // Validate the incoming files
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx|max:5120', // Adjust file types and size limit as needed
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $uploadedFiles = [];
 | 
			
		||||
        $baseDirectory = 'profiles/' . $name;
 | 
			
		||||
        $othersDirectory = $baseDirectory . '/others';
 | 
			
		||||
 | 
			
		||||
        // Check if the base directory exists, if not create it
 | 
			
		||||
        if (!Storage::disk('public')->exists($baseDirectory)) {
 | 
			
		||||
            Storage::disk('public')->makeDirectory($baseDirectory);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the "others" directory exists, if not create it
 | 
			
		||||
        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
			
		||||
            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
			
		||||
        }
 | 
			
		||||
        if ($request->hasFile('files')) {
 | 
			
		||||
            foreach ($request->file('files') as $file) {
 | 
			
		||||
                // Store the file and get its path
 | 
			
		||||
                $originalFilename = $file->getClientOriginalName();
 | 
			
		||||
                if (strpos($originalFilename, '__') === 0) {
 | 
			
		||||
                    // Store the file in the "others" directory
 | 
			
		||||
                    $path = $file->storeAs($othersDirectory, $originalFilename, 'public');
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Store the file in the base directory
 | 
			
		||||
                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
			
		||||
                }
 | 
			
		||||
                $uploadedFiles[] = $path;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response()->json([
 | 
			
		||||
            'status' => true,
 | 
			
		||||
            'message' => 'Files uploaded successfully',
 | 
			
		||||
            'files' => $uploadedFiles,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeFile(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        // Validate that the file URL is provided in the request
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'file_url' => 'required|string',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Get the full file URL from the request
 | 
			
		||||
        $fileUrl = $request->input('file_url');
 | 
			
		||||
 | 
			
		||||
        // Parse the file path from the URL (remove the base URL part)
 | 
			
		||||
        $storagePath = parse_url($fileUrl, PHP_URL_PATH); // Extract the path part of the URL
 | 
			
		||||
        $filePath = str_replace(env('APP_ENV') === 'local' ? '/storage/' : '/image/storage/', '', $storagePath); // Remove "/storage/" to get the actual file path
 | 
			
		||||
 | 
			
		||||
        // Check if the file exists before attempting to delete it
 | 
			
		||||
        if (Storage::disk('public')->exists($filePath)) {
 | 
			
		||||
            // Delete the file
 | 
			
		||||
            Storage::disk('public')->delete($filePath);
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => true,
 | 
			
		||||
                'message' => 'File deleted successfully',
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return response()->json([
 | 
			
		||||
            'status' => false,
 | 
			
		||||
            'message' => 'File not found',
 | 
			
		||||
        ], 404);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -284,7 +284,7 @@ class TicketController extends Controller
 | 
			
		|||
        $rules = [
 | 
			
		||||
            'ticket_id' => 'required',
 | 
			
		||||
            'action' => 'required',
 | 
			
		||||
            'admin_note' => 'required'
 | 
			
		||||
            // 'admin_note' => 'required'
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // Validate the request
 | 
			
		||||
| 
						 | 
				
			
			@ -344,13 +344,13 @@ class TicketController extends Controller
 | 
			
		|||
                    $date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
 | 
			
		||||
 | 
			
		||||
                    //Default: ALL
 | 
			
		||||
                    $start = $date->copy()->setTime(7, 31, 11);
 | 
			
		||||
                    $end = $date->copy()->setTime(17, 1, 11);
 | 
			
		||||
                    $start = $date->copy()->setTime(7, 30, 0);
 | 
			
		||||
                    $end = $date->copy()->setTime(17, 0, 0);
 | 
			
		||||
 | 
			
		||||
                    if ($type == 'S') {
 | 
			
		||||
                        $end = $date->copy()->setTime(11, 31, 11);
 | 
			
		||||
                        $end = $date->copy()->setTime(11, 30, 0);
 | 
			
		||||
                    } else if ($type == 'C') {
 | 
			
		||||
                        $start = $date->copy()->setTime(11, 31, 11);
 | 
			
		||||
                        $start = $date->copy()->setTime(11, 30, 0);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    Tracking::insert([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,8 @@ use Illuminate\Support\Facades\DB;
 | 
			
		|||
use Modules\Admin\app\Models\Admin;
 | 
			
		||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
 | 
			
		||||
use Modules\Admin\app\Models\Tracking;
 | 
			
		||||
use Maatwebsite\Excel\Facades\Excel;
 | 
			
		||||
use App\Exports\TimekeepingExport;
 | 
			
		||||
 | 
			
		||||
class TimekeepingController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -185,4 +187,38 @@ class TimekeepingController extends Controller
 | 
			
		|||
 | 
			
		||||
        return response()->json(['message' => 'Delete fail', 'status' => false]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function export(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        // Validate request
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'month' => 'required|numeric|between:1,12',
 | 
			
		||||
            'year' => 'required|numeric|min:2000',
 | 
			
		||||
            'working_days' => 'required|numeric|between:1,31'
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Reuse get() function to fetch data
 | 
			
		||||
        $response = $this->get($request);
 | 
			
		||||
        $responseData = json_decode($response->getContent(), true);
 | 
			
		||||
 | 
			
		||||
        if (!$responseData['status']) {
 | 
			
		||||
            return response()->json(['status' => false, 'message' => 'No data found']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Lọc chỉ lấy user có permission staff
 | 
			
		||||
        $staffData = array_filter($responseData['data'], function($user) {
 | 
			
		||||
            return isset($user['user']['permission']) && $user['user']['permission'] === 'staff';
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        $currentDate = date('d_His');
 | 
			
		||||
        return Excel::download(
 | 
			
		||||
            new TimekeepingExport(
 | 
			
		||||
                array_values($staffData), // Convert to indexed array after filtering
 | 
			
		||||
                $request->month,
 | 
			
		||||
                $request->year,
 | 
			
		||||
                $request->working_days
 | 
			
		||||
            ),
 | 
			
		||||
            "Timekeeping_{$request->month}_{$request->year}_{$currentDate}.xlsx"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,9 @@
 | 
			
		|||
 | 
			
		||||
namespace Modules\Admin\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Helper\Cache\CurrentMonthTimekeeping;
 | 
			
		||||
use App\Events\WarningChekinLate;
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Mail\CheckinLateMail;
 | 
			
		||||
use App\Traits\AnalyzeData;
 | 
			
		||||
use App\Traits\HasFilterRequest;
 | 
			
		||||
use App\Traits\HasOrderByRequest;
 | 
			
		||||
| 
						 | 
				
			
			@ -11,6 +12,7 @@ use App\Traits\HasSearchRequest;
 | 
			
		|||
use Carbon\Carbon;
 | 
			
		||||
use DateTime;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Mail;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Modules\Admin\app\Models\Admin;
 | 
			
		||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
 | 
			
		||||
| 
						 | 
				
			
			@ -163,14 +165,9 @@ class TrackingController extends Controller
 | 
			
		|||
        $id = $request->get('id');
 | 
			
		||||
 | 
			
		||||
        $tracking = Tracking::find($id);
 | 
			
		||||
        // $id = $request->get('id');
 | 
			
		||||
 | 
			
		||||
        // $tracking = Tracking::find($id);
 | 
			
		||||
        // $payload = $request->all();
 | 
			
		||||
 | 
			
		||||
        // if ($tracking) {
 | 
			
		||||
        //     $tracking->update($payload);
 | 
			
		||||
        // }
 | 
			
		||||
        $user = Admin::find($tracking->user_id);
 | 
			
		||||
        $this->sendCheckinLateMail($user, $tracking->created_at, $tracking->status);
 | 
			
		||||
        if ($request->hasFile('image')) {
 | 
			
		||||
            $file = $request->file('image');
 | 
			
		||||
            $filename = $request->file_name;
 | 
			
		||||
| 
						 | 
				
			
			@ -203,4 +200,54 @@ class TrackingController extends Controller
 | 
			
		|||
            'status' => true
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // public function sendCheckinLateMail()
 | 
			
		||||
    private function sendCheckinLateMail($user, $time, $status)
 | 
			
		||||
    {
 | 
			
		||||
        // $status = $this->CHECK_IN;
 | 
			
		||||
        // $lastCheck =Tracking::find(1)->created_at;
 | 
			
		||||
        // $user = Admin::where('name', 'LE TAN LUAN')->first();
 | 
			
		||||
        if($status === $this->CHECK_IN){
 | 
			
		||||
            $morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE'));
 | 
			
		||||
            $morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE'));
 | 
			
		||||
            $afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE'));
 | 
			
		||||
            $afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE'));
 | 
			
		||||
            $admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email');
 | 
			
		||||
 | 
			
		||||
            if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time))
 | 
			
		||||
            {
 | 
			
		||||
                $period = 'morning';
 | 
			
		||||
                $minutes_late = $morning_time->diffInMinutes($time);
 | 
			
		||||
                $data = array(
 | 
			
		||||
                    "email_template" => "email.checkin_late_notification",
 | 
			
		||||
                    "email" => $user->email,
 | 
			
		||||
                    "name" => $user->name,
 | 
			
		||||
                    "admin_mails" => $admin_mails,
 | 
			
		||||
                    "message1" => "Your ". $period ." starts ". $minutes_late ." minutes late",
 | 
			
		||||
                    "message2" => "You checked in at [" . $time ."]",
 | 
			
		||||
                    "url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in",
 | 
			
		||||
                    "subject" => "[Management System] Late warning - " . $user->name
 | 
			
		||||
                );
 | 
			
		||||
                Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // if ($time->greaterThan($afternoon_condition_time))
 | 
			
		||||
            // {
 | 
			
		||||
            //     $period = 'afternoon';
 | 
			
		||||
            //     $minutes_late = $afternoon_time->diffInMinutes($time);
 | 
			
		||||
            //     $data = array(
 | 
			
		||||
            //         "email_template" => "email.checkin_late_notification",
 | 
			
		||||
            //         "email" => $user->email,
 | 
			
		||||
            //         "name" => $user->name,
 | 
			
		||||
            //         "message1" => "Your ". $period ." starts <b>". $minutes_late ."</b> minutes late",
 | 
			
		||||
            //         "message2" => "You checked in at <b>[" . $time ."]</b>",
 | 
			
		||||
            //         "url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in",
 | 
			
		||||
            //         "subject" => "[Management System] Late warning"
 | 
			
		||||
            //     );
 | 
			
		||||
            //     Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -120,12 +120,13 @@ Route::middleware('api')
 | 
			
		|||
            Route::group([
 | 
			
		||||
                'prefix' => 'timekeeping',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
 | 
			
		||||
                Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
 | 
			
		||||
                Route::get('/export', [TimekeepingController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +146,8 @@ Route::middleware('api')
 | 
			
		|||
            Route::group([
 | 
			
		||||
                'prefix' => 'leave-management',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff');
 | 
			
		||||
                Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +161,14 @@ Route::middleware('api')
 | 
			
		|||
                Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
                'prefix' => 'profile',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
                'prefix' => 'criterias',
 | 
			
		||||
            ], function () {
 | 
			
		||||
| 
						 | 
				
			
			@ -212,4 +222,5 @@ Route::group([
 | 
			
		|||
    'prefix' => 'v1/admin/jira',
 | 
			
		||||
], function () {
 | 
			
		||||
    Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
 | 
			
		||||
    Route::get('/send-warning-mail', [JiraController::class, 'sendWarningMailByAllowcation']);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Console\Commands;
 | 
			
		||||
 | 
			
		||||
use App\Jobs\CheckUserAttendanceJob;
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
 | 
			
		||||
class CheckUserAttendanceCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
    // Định nghĩa command signature
 | 
			
		||||
    protected $signature = 'attendance:check {period?}';
 | 
			
		||||
    protected $description = 'Kiểm tra check in và check out của người dùng và tạo ticket nếu thiếu';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new command instance.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the console command.
 | 
			
		||||
     */
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        // Lấy argument period (Sáng "S" hoặc Chiều "C")
 | 
			
		||||
        $period = $this->argument('period');
 | 
			
		||||
 | 
			
		||||
        // Dispatch job để kiểm tra check in và check out
 | 
			
		||||
        CheckUserAttendanceJob::dispatch($period);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,12 @@ class Kernel extends ConsoleKernel
 | 
			
		|||
        // 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('leave:deduct')->yearlyOn(3, 31, '23:59:59');
 | 
			
		||||
 | 
			
		||||
        // Chạy buổi sáng lúc 12:00
 | 
			
		||||
        $schedule->command('attendance:check S')->dailyAt('12:00');
 | 
			
		||||
 | 
			
		||||
        // Chạy buổi chiều lúc 17:30
 | 
			
		||||
        $schedule->command('attendance:check C')->dailyAt('17:30');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Exports;
 | 
			
		||||
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\FromArray;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\WithHeadings;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\WithStyles;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Style\Border;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
 | 
			
		||||
 | 
			
		||||
class LeaveManagementExport implements FromArray, WithHeadings, WithStyles
 | 
			
		||||
{
 | 
			
		||||
    protected $data;
 | 
			
		||||
    protected $year;
 | 
			
		||||
 | 
			
		||||
    public function __construct($data)
 | 
			
		||||
    {
 | 
			
		||||
        $this->data = $data;
 | 
			
		||||
        $this->year = Carbon::now()->year;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function headings(): array
 | 
			
		||||
    {
 | 
			
		||||
        $months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
 | 
			
		||||
        return array_merge(
 | 
			
		||||
            ['No.', 'User'],
 | 
			
		||||
            $months,
 | 
			
		||||
            ['Total', 'Off', 'Remaining', 'Notes']
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function array(): array
 | 
			
		||||
    {
 | 
			
		||||
        $headers = $this->headings(); // Lấy tiêu đề
 | 
			
		||||
 | 
			
		||||
        $rows = [];
 | 
			
		||||
        foreach ($this->data as $index => $user) {
 | 
			
		||||
            $totalDayOff = 0;
 | 
			
		||||
            $totalDayLeave = $user['leaveDay']['ld_day'] + $user['leaveDay']['ld_date_additional'];
 | 
			
		||||
 | 
			
		||||
            // Tính tổng ngày nghỉ theo tháng
 | 
			
		||||
            $monthlyLeaves = array_fill(1, 12, 0);
 | 
			
		||||
            foreach ($user['monthlyLeaveDays'] as $leaveDay) {
 | 
			
		||||
                $monthlyLeaves[$leaveDay['month']] += $leaveDay['leave_days'];
 | 
			
		||||
                $totalDayOff += $leaveDay['leave_days'];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Tạo dòng dữ liệu
 | 
			
		||||
            $row = [
 | 
			
		||||
                $index + 1,
 | 
			
		||||
                $user['user']['name']
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            // Thêm dữ liệu các tháng
 | 
			
		||||
            for ($month = 1; $month <= 12; $month++) {
 | 
			
		||||
                $row[] = $monthlyLeaves[$month] ?: '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Thêm tổng số ngày
 | 
			
		||||
            $row[] = $totalDayLeave;
 | 
			
		||||
            $row[] = $totalDayOff;
 | 
			
		||||
            $row[] = $totalDayLeave - $totalDayOff;
 | 
			
		||||
            $row[] = $user['leaveDay']['ld_note'] ?? '';
 | 
			
		||||
 | 
			
		||||
            $rows[] = $row;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return array_merge([$headers], $rows); // Thêm tiêu đề vào đầu mảng
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function styles(Worksheet $sheet)
 | 
			
		||||
    {
 | 
			
		||||
        $lastRow = count($this->data) + 2;
 | 
			
		||||
        $lastColumn = 'R';
 | 
			
		||||
 | 
			
		||||
        // Thêm và style title
 | 
			
		||||
        $sheet->mergeCells("A1:{$lastColumn}1");
 | 
			
		||||
        $sheet->setCellValue('A1', "DANH SÁCH NGÀY NGHỈ NĂM {$this->year}");
 | 
			
		||||
        $sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([
 | 
			
		||||
            'font' => [
 | 
			
		||||
                'bold' => true,
 | 
			
		||||
                'size' => 14
 | 
			
		||||
            ],
 | 
			
		||||
            'alignment' => [
 | 
			
		||||
                'horizontal' => Alignment::HORIZONTAL_CENTER,
 | 
			
		||||
                'vertical' => Alignment::VERTICAL_CENTER
 | 
			
		||||
            ]
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Style cho header (dời xuống row 2)
 | 
			
		||||
        $sheet->getStyle("A2:{$lastColumn}2")->applyFromArray([
 | 
			
		||||
            'font' => ['bold' => true],
 | 
			
		||||
            'alignment' => [
 | 
			
		||||
                'horizontal' => Alignment::HORIZONTAL_CENTER,
 | 
			
		||||
                'vertical' => Alignment::VERTICAL_CENTER
 | 
			
		||||
            ]
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Style cho toàn bộ bảng (bắt đầu từ row 1)
 | 
			
		||||
        $sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray([
 | 
			
		||||
            'borders' => [
 | 
			
		||||
                'allBorders' => [
 | 
			
		||||
                    'borderStyle' => Border::BORDER_THIN
 | 
			
		||||
                ]
 | 
			
		||||
            ],
 | 
			
		||||
            'alignment' => [
 | 
			
		||||
                'vertical' => Alignment::VERTICAL_CENTER
 | 
			
		||||
            ]
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Căn giữa cho các cột số liệu (điều chỉnh range bắt đầu từ row 3)
 | 
			
		||||
        for ($col = 'C'; $col <= 'P'; $col++) {
 | 
			
		||||
            $sheet->getStyle("{$col}3:{$col}{$lastRow}")
 | 
			
		||||
                ->getAlignment()
 | 
			
		||||
                ->setHorizontal(Alignment::HORIZONTAL_CENTER);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set độ rộng cột
 | 
			
		||||
        $sheet->getColumnDimension('A')->setWidth(5);  // No.
 | 
			
		||||
        $sheet->getColumnDimension('B')->setWidth(30); // User
 | 
			
		||||
        // Các tháng
 | 
			
		||||
        for ($i = 'C'; $i <= 'N'; $i++) {
 | 
			
		||||
            $sheet->getColumnDimension($i)->setWidth(8);
 | 
			
		||||
        }
 | 
			
		||||
        $sheet->getColumnDimension('O')->setWidth(8);  // Total
 | 
			
		||||
        $sheet->getColumnDimension('P')->setWidth(8);  // Off
 | 
			
		||||
        $sheet->getColumnDimension('Q')->setWidth(12); // Remaining
 | 
			
		||||
        $sheet->getColumnDimension('R')->setWidth(30); // Notes
 | 
			
		||||
 | 
			
		||||
        return $sheet;
 | 
			
		||||
    }
 | 
			
		||||
} 
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,184 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Exports;
 | 
			
		||||
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\FromArray;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\WithHeadings;
 | 
			
		||||
use Maatwebsite\Excel\Concerns\WithStyles;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Style\Border;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
 | 
			
		||||
 | 
			
		||||
class TimekeepingExport implements FromArray, WithHeadings, WithStyles
 | 
			
		||||
{
 | 
			
		||||
    protected $data;
 | 
			
		||||
    protected $month;
 | 
			
		||||
    protected $year;
 | 
			
		||||
    protected $workingDays;
 | 
			
		||||
    protected $daysInMonth;
 | 
			
		||||
 | 
			
		||||
    public function __construct($data, $month, $year, $workingDays)
 | 
			
		||||
    {
 | 
			
		||||
        $this->data = $data;
 | 
			
		||||
        $this->month = $month;
 | 
			
		||||
        $this->year = $year;
 | 
			
		||||
        $this->workingDays = $workingDays;
 | 
			
		||||
        $this->daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function array(): array
 | 
			
		||||
    {
 | 
			
		||||
        // Lấy headers
 | 
			
		||||
        $headers = $this->headings();
 | 
			
		||||
        
 | 
			
		||||
        // Lấy dữ liệu người dùng
 | 
			
		||||
        $userRows = [];
 | 
			
		||||
        foreach ($this->data as $user) {
 | 
			
		||||
            // Kiểm tra permission staff
 | 
			
		||||
            if (!isset($user['user']['permission']) || $user['user']['permission'] !== 'staff') {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $row = [
 | 
			
		||||
                $user['user']['name'] ?? 'Unknown',
 | 
			
		||||
                0, // Total days
 | 
			
		||||
                $this->workingDays, // Off days (initialize with working days)
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            $totalDays = 0;
 | 
			
		||||
            // Add data for each day in month
 | 
			
		||||
            for ($day = 1; $day <= $this->daysInMonth; $day++) {
 | 
			
		||||
                $dayData = '';
 | 
			
		||||
                if (isset($user['history'])) {
 | 
			
		||||
                    foreach ($user['history'] as $history) {
 | 
			
		||||
                        if ($history['day'] === $day) {
 | 
			
		||||
                            $total = $history['total'] ?? 0;
 | 
			
		||||
                            if ($total >= 7 * 3600) {
 | 
			
		||||
                                $dayData = '1';
 | 
			
		||||
                                $totalDays += 1;
 | 
			
		||||
                            } else if ($total >= 3.5 * 3600) {
 | 
			
		||||
                                $dayData = '0.5';
 | 
			
		||||
                                $totalDays += 0.5;
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $row[] = $dayData;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Update total and off days
 | 
			
		||||
            $row[1] = $totalDays;
 | 
			
		||||
            $row[2] = $this->workingDays - $totalDays;
 | 
			
		||||
            
 | 
			
		||||
            // Add Notes column with formatted content
 | 
			
		||||
            $notes = [];
 | 
			
		||||
            if (isset($user['history'])) {
 | 
			
		||||
                foreach ($user['history'] as $history) {
 | 
			
		||||
                    if (!empty($history['notes'])) {
 | 
			
		||||
                        $dayNotes = [];
 | 
			
		||||
                        foreach ($history['notes'] as $note) {
 | 
			
		||||
                            $dayNotes[] = "- {$note['reasonName']} ({$note['timeTypeName']}): {$note['note']}";
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!empty($dayNotes)) {
 | 
			
		||||
                            $notes[] = "Day {$history['day']}:\n" . implode("\n", $dayNotes);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            $row[] = !empty($notes) ? implode("\n\n", $notes) : '';
 | 
			
		||||
            
 | 
			
		||||
            $userRows[] = $row;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return array_merge($headers, $userRows);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function headings(): array
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        $firstRow = ['Day', '', ''];
 | 
			
		||||
        // Second row: Day of week
 | 
			
		||||
        $secondRow = ['', '', ''];
 | 
			
		||||
 | 
			
		||||
        $date = Carbon::create($this->year, $this->month, 1);
 | 
			
		||||
        
 | 
			
		||||
        for ($day = 1; $day <= $this->daysInMonth; $day++) {
 | 
			
		||||
            $firstRow[] = $day;
 | 
			
		||||
            $secondRow[] = $date->format('D');
 | 
			
		||||
            $date->addDay();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add Notes column
 | 
			
		||||
        $firstRow[] = 'Notes';
 | 
			
		||||
        $secondRow[] = '';
 | 
			
		||||
 | 
			
		||||
        return [$firstRow, $secondRow];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function styles(Worksheet $sheet)
 | 
			
		||||
    {
 | 
			
		||||
        $lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
 | 
			
		||||
 | 
			
		||||
        // Title and working days
 | 
			
		||||
        $sheet->mergeCells("A1:{$lastColumn}1");
 | 
			
		||||
        $sheet->setCellValue('A1', "DANH SÁCH CHẤM CÔNG THÁNG {$this->month} NĂM {$this->year}");
 | 
			
		||||
        $sheet->mergeCells("A2:{$lastColumn}2");
 | 
			
		||||
        $sheet->setCellValue('A2', "Số ngày làm việc: {$this->workingDays}");
 | 
			
		||||
 | 
			
		||||
        // Merge cells for "Day" title and set value
 | 
			
		||||
        $sheet->mergeCells("A3:C3");
 | 
			
		||||
        $sheet->setCellValue('A3', 'Day');
 | 
			
		||||
 | 
			
		||||
        // Set values for A4, B4, C4
 | 
			
		||||
        $sheet->setCellValue('A4', 'User');
 | 
			
		||||
        $sheet->setCellValue('B4', 'Total');
 | 
			
		||||
        $sheet->setCellValue('C4', 'Off');
 | 
			
		||||
 | 
			
		||||
        // Calculate last row (2 title rows + 2 header rows + data rows)
 | 
			
		||||
        $lastRow = count($this->data) + 4;
 | 
			
		||||
 | 
			
		||||
        // Styling
 | 
			
		||||
        $sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->setSize(14);
 | 
			
		||||
        $sheet->getStyle("A2:{$lastColumn}2")->getFont()->setBold(true);
 | 
			
		||||
        $sheet->getStyle("A3:{$lastColumn}4")->getFont()->setBold(true);
 | 
			
		||||
 | 
			
		||||
        // Border style
 | 
			
		||||
        $borderStyle = [
 | 
			
		||||
            'borders' => [
 | 
			
		||||
                'allBorders' => [
 | 
			
		||||
                    'borderStyle' => Border::BORDER_THIN,
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // Apply borders to the data area
 | 
			
		||||
        $sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray($borderStyle);
 | 
			
		||||
 | 
			
		||||
        // Center align all cells except Notes column
 | 
			
		||||
        $sheet->getStyle("A1:{$lastColumn}{$lastRow}")
 | 
			
		||||
            ->getAlignment()
 | 
			
		||||
            ->setHorizontal(Alignment::HORIZONTAL_CENTER)
 | 
			
		||||
            ->setVertical(Alignment::VERTICAL_CENTER);
 | 
			
		||||
 | 
			
		||||
        // Left align Notes column
 | 
			
		||||
        $noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
 | 
			
		||||
        $sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
 | 
			
		||||
            ->getAlignment()
 | 
			
		||||
            ->setHorizontal(Alignment::HORIZONTAL_LEFT);
 | 
			
		||||
 | 
			
		||||
        // Set column widths
 | 
			
		||||
        $sheet->getColumnDimension('A')->setWidth(30);
 | 
			
		||||
        $sheet->getColumnDimension('B')->setWidth(10);
 | 
			
		||||
        $sheet->getColumnDimension('C')->setWidth(10);
 | 
			
		||||
        for ($i = 4; $i <= $this->daysInMonth + 3; $i++) {
 | 
			
		||||
            $sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setWidth(5);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set width for Note column
 | 
			
		||||
        $sheet->getColumnDimension($noteColumn)->setWidth(40);
 | 
			
		||||
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
} 
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,197 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Jobs;
 | 
			
		||||
 | 
			
		||||
use App\Mail\TicketMail;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
			
		||||
use Illuminate\Queue\InteractsWithQueue;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
use Illuminate\Support\Facades\Mail;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\CategoryController;
 | 
			
		||||
use Modules\Admin\app\Models\Admin;
 | 
			
		||||
use Modules\Admin\app\Models\Ticket;
 | 
			
		||||
use Modules\Admin\app\Models\Tracking;
 | 
			
		||||
 | 
			
		||||
class CheckUserAttendanceJob implements ShouldQueue
 | 
			
		||||
{
 | 
			
		||||
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 | 
			
		||||
 | 
			
		||||
    protected $period;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new job instance.
 | 
			
		||||
     * @param string|null $period
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($period = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->period = $period;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the job.
 | 
			
		||||
     */
 | 
			
		||||
    public function handle()
 | 
			
		||||
    {
 | 
			
		||||
        // Lấy tất cả người dùng
 | 
			
		||||
        $users = User::where('permission', 'not like', '%admin%')
 | 
			
		||||
                ->where('permission', 'not like', '%accountant%')->get();
 | 
			
		||||
        foreach ($users as $key => $user) {
 | 
			
		||||
            // if ($user->id != 4) {
 | 
			
		||||
            //     continue;
 | 
			
		||||
            // }
 | 
			
		||||
            // Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C')
 | 
			
		||||
            if ($this->period === 'S') {
 | 
			
		||||
                $this->checkMorning($user);
 | 
			
		||||
            } elseif ($this->period === 'C') {
 | 
			
		||||
                $this->checkAfternoon($user);
 | 
			
		||||
            } else {
 | 
			
		||||
                if ($this->period == null) {
 | 
			
		||||
                    // Nếu không có period, kiểm tra cả sáng và chiều
 | 
			
		||||
                    $this->checkMorning($user);
 | 
			
		||||
                    $this->checkAfternoon($user);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function checkAttendance($user, $periodCode, $startTime, $endTime)
 | 
			
		||||
    {
 | 
			
		||||
        $today = Carbon::today();
 | 
			
		||||
        $userId = $user->id;
 | 
			
		||||
        // Lấy tất cả tracking của user trong khoảng thời gian được chỉ định
 | 
			
		||||
        $records = Tracking::where('user_id', $userId)
 | 
			
		||||
            ->whereBetween('time_string', [$startTime, $endTime])
 | 
			
		||||
            ->get();
 | 
			
		||||
 | 
			
		||||
        $hasCheckIn = $records->where('status', 'check in')->isNotEmpty();
 | 
			
		||||
        $hasCheckOut = $records->where('status', 'check out')->isNotEmpty();
 | 
			
		||||
 | 
			
		||||
        // Kiểm tra nếu đã có ticket chờ xử lý hoặc đã được xác nhận, cho cả sáng và chiều
 | 
			
		||||
        $existingTicket = Ticket::where('user_id', $userId)
 | 
			
		||||
            ->where(function ($query) use ($today, $periodCode) {
 | 
			
		||||
                // Check for tickets that exactly cover the current day and period
 | 
			
		||||
                $query->where(function ($subQuery) use ($today, $periodCode) {
 | 
			
		||||
                    $subQuery->where('start_date', $today->format('Y-m-d'))
 | 
			
		||||
                        ->where('end_date', $today->format('Y-m-d'))
 | 
			
		||||
                        ->where(function ($periodQuery) use ($periodCode) {
 | 
			
		||||
                            // Check if the ticket covers the current period
 | 
			
		||||
                            $periodQuery->where('start_period', $periodCode)
 | 
			
		||||
                                ->orWhere(function ($query) {
 | 
			
		||||
                                    // Check for a full-day ticket (S -> C)
 | 
			
		||||
                                    $query->where('start_period', 'S')
 | 
			
		||||
                                        ->where('end_period', 'C');
 | 
			
		||||
                                });
 | 
			
		||||
                        });
 | 
			
		||||
                })
 | 
			
		||||
                    // Check for tickets that span multiple days
 | 
			
		||||
                    ->orWhere(function ($subQuery) use ($today, $periodCode) {
 | 
			
		||||
                        $subQuery->where('start_date', '<=', $today->format('Y-m-d'))
 | 
			
		||||
                            ->where('end_date', '>=', $today->format('Y-m-d'))
 | 
			
		||||
                            ->where(function ($periodQuery) use ($periodCode, $today) {
 | 
			
		||||
                                $periodQuery->where(function ($query)  use ($today) {
 | 
			
		||||
                                    $query->where('start_date', '<', $today->format('Y-m-d'))
 | 
			
		||||
                                        ->where('end_date', '>', $today->format('Y-m-d'));
 | 
			
		||||
                                })
 | 
			
		||||
                                    ->orWhere(function ($query) use ($today, $periodCode) {
 | 
			
		||||
                                        $query->where(function ($queryDateStart) use ($today, $periodCode) {
 | 
			
		||||
                                            $queryDateStart->where('start_date', '=', $today->format('Y-m-d'))
 | 
			
		||||
                                                ->where('end_date', '>', $today->format('Y-m-d'))
 | 
			
		||||
                                                ->where(function ($queryDateStartChild) use ($periodCode) {
 | 
			
		||||
                                                    $queryDateStartChild->where('start_period', $periodCode)
 | 
			
		||||
                                                        ->orWhere(function ($query) {
 | 
			
		||||
                                                            $query->where('start_period', 'S');
 | 
			
		||||
                                                        });
 | 
			
		||||
                                                });
 | 
			
		||||
                                        })
 | 
			
		||||
                                            ->orWhere(function ($queryDateEnd) use ($today, $periodCode) {
 | 
			
		||||
                                                $queryDateEnd->where('end_date', '=', $today->format('Y-m-d'))
 | 
			
		||||
                                                    ->where('start_date', '<', $today->format('Y-m-d'))
 | 
			
		||||
                                                    ->where(function ($queryDateStartChild) use ($periodCode) {
 | 
			
		||||
                                                        $queryDateStartChild->where('end_period', $periodCode)
 | 
			
		||||
                                                            ->orWhere(function ($query) {
 | 
			
		||||
                                                                $query->where('end_period', 'C');
 | 
			
		||||
                                                            });
 | 
			
		||||
                                                    });
 | 
			
		||||
                                            });
 | 
			
		||||
                                    });
 | 
			
		||||
                            });
 | 
			
		||||
                    });
 | 
			
		||||
            })
 | 
			
		||||
            ->whereIn('status', ['WAITING', 'CONFIRMED'])
 | 
			
		||||
            ->first();
 | 
			
		||||
 | 
			
		||||
        $type = 'ONLEAVE';
 | 
			
		||||
        $reason = 'KHONG PHEP';
 | 
			
		||||
 | 
			
		||||
        //Check ngày hợp lệ nếu có check và check out của user khác
 | 
			
		||||
        $dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])
 | 
			
		||||
            ->get();
 | 
			
		||||
        $hasCheckIndateNow = $dateNow->where('status', 'check in')->isNotEmpty();
 | 
			
		||||
        $hasCheckOutdateNow = $dateNow->where('status', 'check out')->isNotEmpty();
 | 
			
		||||
 | 
			
		||||
        // Nếu không có check-in hoặc check-out, và chưa có ticket, tạo ticket mới
 | 
			
		||||
        if (($hasCheckIndateNow && $hasCheckOutdateNow) && !$hasCheckIn && !$hasCheckOut && !$existingTicket) {
 | 
			
		||||
            Ticket::create([
 | 
			
		||||
                'user_id' => $userId,
 | 
			
		||||
                'start_date' => $today->format('Y-m-d'),
 | 
			
		||||
                'start_period' => $periodCode,
 | 
			
		||||
                'end_date' => $today->format('Y-m-d'),
 | 
			
		||||
                'end_period' => $periodCode,
 | 
			
		||||
                'type' => $type,
 | 
			
		||||
                'reason' => $reason,
 | 
			
		||||
                'status' => 'WAITING',
 | 
			
		||||
                'created_at' => Carbon::now(),
 | 
			
		||||
                'updated_at' => Carbon::now()
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            //Send mail
 | 
			
		||||
            $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
 | 
			
		||||
            $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
 | 
			
		||||
            $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
 | 
			
		||||
 | 
			
		||||
            $formattedStartDate = $today->format('d/m/Y');
 | 
			
		||||
            $formattedEndDate = $today->format('d/m/Y');
 | 
			
		||||
 | 
			
		||||
            $admins = Admin::where('permission', 'like', '%admin%')->get();
 | 
			
		||||
            foreach ($admins as $key => $value) {
 | 
			
		||||
                $data = array(
 | 
			
		||||
                    "email_template" => "email.notification_tickets",
 | 
			
		||||
                    "email" => $user->email,
 | 
			
		||||
                    "name" => $user->name,
 | 
			
		||||
                    "date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
 | 
			
		||||
                    "type" => $dataMasterType->c_name,
 | 
			
		||||
                    "note" => $reason,
 | 
			
		||||
                    "link" => "/tickets-management", //link đến page admin
 | 
			
		||||
                    "subject" => "[Ticket request] Ticket From " . $user->name
 | 
			
		||||
                );
 | 
			
		||||
                Mail::to($value->email)->send(new TicketMail($data));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Kiểm tra check-in/check-out buổi sáng và tạo ticket nếu thiếu.
 | 
			
		||||
     * @param int $userId
 | 
			
		||||
     */
 | 
			
		||||
    public function checkMorning($user)
 | 
			
		||||
    {
 | 
			
		||||
        $startTime = Carbon::today()->setTime(6, 0);  // Thời gian bắt đầu buổi sáng
 | 
			
		||||
        $endTime = Carbon::createFromTime(12, 0);     // Thời gian kết thúc buổi sáng
 | 
			
		||||
        $this->checkAttendance($user, 'S', $startTime, $endTime);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Kiểm tra check-in/check-out buổi chiều và tạo ticket nếu thiếu.
 | 
			
		||||
     * @param int $userId
 | 
			
		||||
     */
 | 
			
		||||
    public function checkAfternoon($user)
 | 
			
		||||
    {
 | 
			
		||||
        $startTime = Carbon::today()->setTime(12, 0);  // Thời gian bắt đầu buổi chiều
 | 
			
		||||
        $endTime = Carbon::createFromTime(17, 30);     // Thời gian kết thúc buổi chiều
 | 
			
		||||
        $this->checkAttendance($user, 'C', $startTime, $endTime);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ class DeductLeaveDays implements ShouldQueue
 | 
			
		|||
                        ]);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                //Nếu không sử dụng ngày nghỉ còn lại ở nằm rồi thì xóa => theo luật ld
 | 
			
		||||
                //Nếu không sử dụng ngày nghỉ còn lại ở năm rồi thì xóa => theo luật ld
 | 
			
		||||
                LeaveDays::where('ld_year', $this->year)
 | 
			
		||||
                    ->where('ld_user_id', $user->id)
 | 
			
		||||
                    ->update([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Mail;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Mail\Mailable;
 | 
			
		||||
use Illuminate\Mail\Mailables\Content;
 | 
			
		||||
use Illuminate\Mail\Mailables\Envelope;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
use Illuminate\Mail\Mailables\Address;
 | 
			
		||||
 | 
			
		||||
class CheckinLateMail extends Mailable
 | 
			
		||||
{
 | 
			
		||||
    use Queueable, SerializesModels;
 | 
			
		||||
    public $data;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new message instance.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($data)
 | 
			
		||||
    {
 | 
			
		||||
        $this->data = $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the message envelope.
 | 
			
		||||
     */
 | 
			
		||||
    public function envelope(): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        return new Envelope(
 | 
			
		||||
            // replyTo: [
 | 
			
		||||
            //     new Address($this->data["email"], $this->data["name"]),
 | 
			
		||||
            // ],
 | 
			
		||||
            subject: $this->data["subject"],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the message content definition.
 | 
			
		||||
     */
 | 
			
		||||
    public function content(): Content
 | 
			
		||||
    {
 | 
			
		||||
        return new Content(
 | 
			
		||||
            view: ($this->data["email_template"])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the attachments for the message.
 | 
			
		||||
     *
 | 
			
		||||
     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 | 
			
		||||
     */
 | 
			
		||||
    public function attachments(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Mail;
 | 
			
		||||
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Bus\Queueable;
 | 
			
		||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
			
		||||
use Illuminate\Mail\Mailable;
 | 
			
		||||
use Illuminate\Mail\Mailables\Content;
 | 
			
		||||
use Illuminate\Mail\Mailables\Envelope;
 | 
			
		||||
use Illuminate\Queue\SerializesModels;
 | 
			
		||||
 | 
			
		||||
class WarningLongTask extends Mailable
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
    use Queueable, SerializesModels;
 | 
			
		||||
 | 
			
		||||
    public $data;
 | 
			
		||||
 | 
			
		||||
   /**
 | 
			
		||||
     * Create a new message instance.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($data)
 | 
			
		||||
    {
 | 
			
		||||
        $this->data = $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the message envelope.
 | 
			
		||||
     */
 | 
			
		||||
    public function envelope(): Envelope
 | 
			
		||||
    {
 | 
			
		||||
        return new Envelope(
 | 
			
		||||
            subject: '[SYSTEM] - Task are forgotten - '.$this->data[0]['fields']['assignee']['displayName'],
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function build(): self
 | 
			
		||||
    {
 | 
			
		||||
        return $this->view('email.warningLongTask', ['data'=> $this->data]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -259,7 +259,7 @@ class JiraService
 | 
			
		|||
            foreach ($issues['issues'] as $issue) {
 | 
			
		||||
                $projectName = $issue['fields']['project']['name'];
 | 
			
		||||
                $username = $issue['fields']['assignee']['displayName'];
 | 
			
		||||
 | 
			
		||||
                $issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
 | 
			
		||||
                if (!isset($groupedIssues[$projectName])) {
 | 
			
		||||
                    $groupedIssues[$projectName] = [];
 | 
			
		||||
                    $groupedIssues[$projectName]['project'] = $issue['fields']['project'];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ return [
 | 
			
		|||
    |
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    'paths' => ['api/*', 'sanctum/csrf-cookie'],
 | 
			
		||||
    'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
 | 
			
		||||
 | 
			
		||||
    'allowed_methods' => ['*'],
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,153 @@
 | 
			
		|||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            font-family: "Arial", sans-serif;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            min-height: 100vh;
 | 
			
		||||
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            background-color: #f7fafc;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .content {
 | 
			
		||||
            background-color: #fffefe;
 | 
			
		||||
            padding: 2rem 0;
 | 
			
		||||
            border-radius: 0.5rem;
 | 
			
		||||
            width: 100vw;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .my-0 {
 | 
			
		||||
            margin-bottom: 0;
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
    <title>{{ $data['subject'] }}</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
    <body style="
 | 
			
		||||
        font-family: Arial, Helvetica, sans-serif;
 | 
			
		||||
        background-color: #edf2f7;
 | 
			
		||||
    ">
 | 
			
		||||
        <table style="margin: 0 auto">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td align="center" valign="top" style="padding: 36px 24px;">
 | 
			
		||||
                                <a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
 | 
			
		||||
                                    <img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
 | 
			
		||||
                                        alt="Logo" border="0" width="100"
 | 
			
		||||
                                        style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
                        background-color: #ffffff;
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                        padding: 24px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
 | 
			
		||||
                                    Hi {{ $data['name'] }},
 | 
			
		||||
                                </h3>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
 | 
			
		||||
                                    {{ $data['message1'] }}
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
 | 
			
		||||
                                    {{ $data['message2'] }}
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
 | 
			
		||||
                                    <a href="{{ $data['url'] }}"
 | 
			
		||||
                                        style="
 | 
			
		||||
                                  color: #fff;
 | 
			
		||||
                                  border-radius: 10px;
 | 
			
		||||
                                  background-color: rgba(68,115,196);
 | 
			
		||||
                                  background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
 | 
			
		||||
                                  text-decoration: none;
 | 
			
		||||
                                  display: inline-block;
 | 
			
		||||
                                  font-weight: 600;
 | 
			
		||||
                                  font-size: 16px;
 | 
			
		||||
                                  line-height: 150%;
 | 
			
		||||
                                  text-align: center;
 | 
			
		||||
                                  margin: 0;
 | 
			
		||||
                                  padding: 10px 12px;
 | 
			
		||||
                                  ">
 | 
			
		||||
                                        Check now</a>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                       
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <h5
 | 
			
		||||
                                    style="
 | 
			
		||||
                                    color: #222222;
 | 
			
		||||
                                    text-align: center;
 | 
			
		||||
                                    padding: 10px 36px;
 | 
			
		||||
                                    margin: 0;
 | 
			
		||||
                                ">
 | 
			
		||||
                                    <p>© 2024 APAC Tech.</p>
 | 
			
		||||
                                </h5>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
    </body>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            font-family: "Arial", sans-serif;
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            min-height: 100vh;
 | 
			
		||||
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
            background-color: #f7fafc;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .content {
 | 
			
		||||
            background-color: #fffefe;
 | 
			
		||||
            padding: 2rem 0;
 | 
			
		||||
            border-radius: 0.5rem;
 | 
			
		||||
            width: 100vw;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .my-0 {
 | 
			
		||||
            margin-bottom: 0;
 | 
			
		||||
            margin-top: 0;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
    <title>[SYSTEM] - Task are forgotten</title>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
    <body style="
 | 
			
		||||
        font-family: Arial, Helvetica, sans-serif;
 | 
			
		||||
        background-color: #edf2f7;
 | 
			
		||||
    ">
 | 
			
		||||
        <table style="margin: 0 auto">
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td align="center" valign="top" style="padding: 36px 24px;">
 | 
			
		||||
                                <a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
 | 
			
		||||
                                    <img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
 | 
			
		||||
                                        alt="Logo" border="0" width="100"
 | 
			
		||||
                                        style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
                        background-color: #ffffff;
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                        padding: 24px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
 | 
			
		||||
                                    Hi {{ $data[0]['fields']['assignee']['displayName'] }},
 | 
			
		||||
                                </h3>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
 | 
			
		||||
                                    You currently have some tasks that have not been updated for a long time. Please check and update them!
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                            @foreach($data as $iss)
 | 
			
		||||
                            <a href="https://apactechvn.atlassian.net/browse/{{$iss['key']}}">{{$iss['fields']['summary']}} - <b>haven't updated in {{$iss['daysRemaining']}} days</b></a><br>
 | 
			
		||||
                            @endforeach
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <table
 | 
			
		||||
                        style="
 | 
			
		||||
                        margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
                        width: 768px;
 | 
			
		||||
                    ">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <h5
 | 
			
		||||
                                    style="
 | 
			
		||||
                                    color: #222222;
 | 
			
		||||
                                    text-align: center;
 | 
			
		||||
                                    padding: 10px 36px;
 | 
			
		||||
                                    margin: 0;
 | 
			
		||||
                                ">
 | 
			
		||||
                                    <p>© 2024 APAC Tech.</p>
 | 
			
		||||
                                </h5>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
    </body>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -11,19 +11,17 @@
 | 
			
		|||
    "test": "npm run jest"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@codemirror/lang-javascript": "^6.2.1",
 | 
			
		||||
    "@mantine/charts": "^7.4.1",
 | 
			
		||||
    "@mantine/core": "7.2.1",
 | 
			
		||||
    "@mantine/dates": "^7.3.2",
 | 
			
		||||
    "@mantine/ds": "^7.2.2",
 | 
			
		||||
    "@mantine/form": "^7.2.2",
 | 
			
		||||
    "@mantine/hooks": "7.2.1",
 | 
			
		||||
    "@mantine/notifications": "^7.2.2",
 | 
			
		||||
    "@mantine/vanilla-extract": "7.2.1",
 | 
			
		||||
    "@reduxjs/toolkit": "^1.9.7",
 | 
			
		||||
    "@tabler/icons-react": "^2.40.0",
 | 
			
		||||
    "@uiw/react-codemirror": "^4.21.21",
 | 
			
		||||
    "@vanilla-extract/css": "^1.13.0",
 | 
			
		||||
    "@codemirror/lang-javascript": "^6.2.2",
 | 
			
		||||
    "@mantine/core": "^7.13.2",
 | 
			
		||||
    "@mantine/dates": "^7.13.2",
 | 
			
		||||
    "@mantine/form": "^7.13.2",
 | 
			
		||||
    "@mantine/hooks": "^7.13.2",
 | 
			
		||||
    "@mantine/notifications": "^7.13.2",
 | 
			
		||||
    "@mantine/vanilla-extract": "^7.13.2",
 | 
			
		||||
    "@reduxjs/toolkit": "^2.3.0",
 | 
			
		||||
    "@tabler/icons-react": "^3.19.0",
 | 
			
		||||
    "@uiw/react-codemirror": "^4.23.5",
 | 
			
		||||
    "@vanilla-extract/css": "^1.16.0",
 | 
			
		||||
    "axios": "^1.6.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "dayjs": "^1.11.10",
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +31,7 @@
 | 
			
		|||
    "jwt-decode": "^4.0.0",
 | 
			
		||||
    "moment": "^2.30.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-doc-viewer": "^0.1.14",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-redux": "^8.1.3",
 | 
			
		||||
    "react-router-dom": "^6.19.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
 | 
			
		|||
 | 
			
		||||
//Timekeeping
 | 
			
		||||
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
 | 
			
		||||
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
 | 
			
		||||
export const updateMultipleUserWorkingTime =
 | 
			
		||||
  API_URL + 'v1/admin/timekeeping/addMutilple'
 | 
			
		||||
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +37,7 @@ export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
 | 
			
		|||
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
 | 
			
		||||
export const updateNoteLeave =
 | 
			
		||||
  API_URL + 'v1/admin/leave-management/saveNoteLeave'
 | 
			
		||||
export const exportLeaveManagement = API_URL + 'v1/admin/leave-management/export'
 | 
			
		||||
 | 
			
		||||
//Tickets
 | 
			
		||||
export const getTickets = API_URL + 'v1/admin/ticket/all'
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +88,10 @@ export const getAllTechByUserId =
 | 
			
		|||
export const evaluation = API_URL + 'v1/admin/evaluation/report'
 | 
			
		||||
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
 | 
			
		||||
 | 
			
		||||
export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files'
 | 
			
		||||
export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile'
 | 
			
		||||
export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
 | 
			
		||||
 | 
			
		||||
//Technical
 | 
			
		||||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
 | 
			
		||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
 | 
			
		||||
 | 
			
		||||
export default function FilePreview() {
 | 
			
		||||
  const docs = [
 | 
			
		||||
    // {
 | 
			
		||||
    //   uri:
 | 
			
		||||
    //     "http://localhost:9000/uploads/ULRYB3ATJ56B/Screenshot%202021-04-28%20at%2014.04.23.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T142426Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=761187860be22801088ab8c212733f7f52af8f62d638f1341ee2ae4c18944251"
 | 
			
		||||
    //   // "http://localhost:9000/uploads/6QK5HJ84MAEM/RAS-118_CompanyCodes__SalesOffices.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T110429Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=c20f9b77ffdc1a15910cea5acd3420b6583a1d4d38ce5716da30f1d0ea4315d5"
 | 
			
		||||
    //   // "https://res.cloudinary.com/cloudinaryforme/image/upload/v1618339571/workplace-1245776_1920_i9ayae.jpg"
 | 
			
		||||
    // },
 | 
			
		||||
 | 
			
		||||
    // {
 | 
			
		||||
    //   uri:
 | 
			
		||||
    //     "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"
 | 
			
		||||
    // },
 | 
			
		||||
    { uri: "https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf" },
 | 
			
		||||
  ];
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="App">
 | 
			
		||||
      <h1>Hello CodeSandbox</h1>
 | 
			
		||||
 | 
			
		||||
      <DocViewer pluginRenderers={DocViewerRenderers} documents={docs} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
// import { render } from 'tests'
 | 
			
		||||
// import Footer from './Footer'
 | 
			
		||||
 | 
			
		||||
// describe('Footer component', () => {
 | 
			
		||||
//   it('renders "Footer"', () => {
 | 
			
		||||
//     render(<Footer />)
 | 
			
		||||
//     // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
 | 
			
		||||
//   })
 | 
			
		||||
// })
 | 
			
		||||
| 
						 | 
				
			
			@ -1,107 +0,0 @@
 | 
			
		|||
import { Text, Container, ActionIcon, Group, rem } from '@mantine/core'
 | 
			
		||||
import {
 | 
			
		||||
  IconBrandTwitter,
 | 
			
		||||
  IconBrandYoutube,
 | 
			
		||||
  IconBrandInstagram,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import { MantineLogo } from '@mantine/ds'
 | 
			
		||||
import * as classes from './FooterLinks.module.css.ts'
 | 
			
		||||
 | 
			
		||||
const data = [
 | 
			
		||||
  {
 | 
			
		||||
    title: 'About',
 | 
			
		||||
    links: [
 | 
			
		||||
      { label: 'Features', link: '#' },
 | 
			
		||||
      { label: 'Pricing', link: '#' },
 | 
			
		||||
      { label: 'Support', link: '#' },
 | 
			
		||||
      { label: 'Forums', link: '#' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: 'Project',
 | 
			
		||||
    links: [
 | 
			
		||||
      { label: 'Contribute', link: '#' },
 | 
			
		||||
      { label: 'Media assets', link: '#' },
 | 
			
		||||
      { label: 'Changelog', link: '#' },
 | 
			
		||||
      { label: 'Releases', link: '#' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: 'Community',
 | 
			
		||||
    links: [
 | 
			
		||||
      { label: 'Join Discord', link: '#' },
 | 
			
		||||
      { label: 'Follow on Twitter', link: '#' },
 | 
			
		||||
      { label: 'Email newsletter', link: '#' },
 | 
			
		||||
      { label: 'GitHub discussions', link: '#' },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
const Footer = () => {
 | 
			
		||||
  const groups = data.map((group) => {
 | 
			
		||||
    const links = group.links.map((link, index) => (
 | 
			
		||||
      <Text<'a'>
 | 
			
		||||
        key={index}
 | 
			
		||||
        className={classes.link}
 | 
			
		||||
        component="a"
 | 
			
		||||
        href={link.link}
 | 
			
		||||
        onClick={(event) => event.preventDefault()}
 | 
			
		||||
      >
 | 
			
		||||
        {link.label}
 | 
			
		||||
      </Text>
 | 
			
		||||
    ))
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classes.wrapper} key={group.title}>
 | 
			
		||||
        <Text className={classes.title}>{group.title}</Text>
 | 
			
		||||
        {links}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <footer className={classes.footer}>
 | 
			
		||||
      <Container className={classes.inner}>
 | 
			
		||||
        <div className={classes.logo}>
 | 
			
		||||
          <MantineLogo size={30} />
 | 
			
		||||
          <Text size="xs" c="dimmed" className={classes.description}>
 | 
			
		||||
            Build fully functional accessible web applications faster than ever
 | 
			
		||||
          </Text>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className={classes.groups}>{groups}</div>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <Container className={classes.afterFooter}>
 | 
			
		||||
        <Text c="dimmed" size="sm">
 | 
			
		||||
          © 2023 APACTECH.dev. All rights reserved.
 | 
			
		||||
        </Text>
 | 
			
		||||
 | 
			
		||||
        <Group
 | 
			
		||||
          gap={0}
 | 
			
		||||
          className={classes.social}
 | 
			
		||||
          justify="flex-end"
 | 
			
		||||
          wrap="nowrap"
 | 
			
		||||
        >
 | 
			
		||||
          <ActionIcon size="lg" color="gray" variant="subtle">
 | 
			
		||||
            <IconBrandTwitter
 | 
			
		||||
              style={{ width: rem(18), height: rem(18) }}
 | 
			
		||||
              stroke={1.5}
 | 
			
		||||
            />
 | 
			
		||||
          </ActionIcon>
 | 
			
		||||
          <ActionIcon size="lg" color="gray" variant="subtle">
 | 
			
		||||
            <IconBrandYoutube
 | 
			
		||||
              style={{ width: rem(18), height: rem(18) }}
 | 
			
		||||
              stroke={1.5}
 | 
			
		||||
            />
 | 
			
		||||
          </ActionIcon>
 | 
			
		||||
          <ActionIcon size="lg" color="gray" variant="subtle">
 | 
			
		||||
            <IconBrandInstagram
 | 
			
		||||
              style={{ width: rem(18), height: rem(18) }}
 | 
			
		||||
              stroke={1.5}
 | 
			
		||||
            />
 | 
			
		||||
          </ActionIcon>
 | 
			
		||||
        </Group>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </footer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Footer
 | 
			
		||||
| 
						 | 
				
			
			@ -1,104 +0,0 @@
 | 
			
		|||
import { rem } from '@mantine/core'
 | 
			
		||||
import { vars } from '@/theme'
 | 
			
		||||
import { style } from '@vanilla-extract/css'
 | 
			
		||||
 | 
			
		||||
export const footer = style({
 | 
			
		||||
  paddingTop: rem(vars.spacing.xl),
 | 
			
		||||
  paddingBottom: rem(vars.spacing.xl),
 | 
			
		||||
  backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
 | 
			
		||||
  borderTop: `${rem(1)} solid light-dark(${vars.colors.gray[2]}, transparent)`,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const logo = style({
 | 
			
		||||
  maxWidth: rem(200),
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      display: 'flex',
 | 
			
		||||
      flexDirection: 'column',
 | 
			
		||||
      alignItems: 'center',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const description = style({
 | 
			
		||||
  marginTop: rem(5),
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      marginTop: rem(vars.spacing.xs),
 | 
			
		||||
      textAlign: 'center',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const inner = style({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  justifyContent: 'space-between',
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      flexDirection: 'column',
 | 
			
		||||
      alignItems: 'center',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const groups = style({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      display: 'none',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const wrapper = style({
 | 
			
		||||
  width: rem(160),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const link = style({
 | 
			
		||||
  display: 'block',
 | 
			
		||||
  color: `light-dark(${vars.colors.gray[6]}, ${vars.colors.dark[1]})`,
 | 
			
		||||
  fontSize: vars.fontSizes.sm,
 | 
			
		||||
  paddingTop: rem(3),
 | 
			
		||||
  paddingBottom: rem(3),
 | 
			
		||||
 | 
			
		||||
  ':hover': {
 | 
			
		||||
    textDecoration: 'underline',
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const title = style({
 | 
			
		||||
  fontSize: vars.fontSizes.lg,
 | 
			
		||||
  fontWeight: 700,
 | 
			
		||||
  fontFamily: `Greycliff CF, ${vars.fontFamily}`,
 | 
			
		||||
  marginBottom: Number(vars.spacing.xs) / 2,
 | 
			
		||||
  color: `light-dark(${vars.colors.black}, ${vars.colors.white})`,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const afterFooter = style({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  justifyContent: 'space-between',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  marginTop: vars.spacing.xl,
 | 
			
		||||
  paddingTop: vars.spacing.xl,
 | 
			
		||||
  paddingBottom: vars.spacing.xl,
 | 
			
		||||
  borderTop: `rem(1) solid light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[4]})`,
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      flexDirection: 'column',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const social = style({
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.sm})`]: {
 | 
			
		||||
      marginTop: vars.spacing.xs,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
import { style, styleVariants } from '@vanilla-extract/css'
 | 
			
		||||
import { vars } from '../../theme'
 | 
			
		||||
import { rem } from '@mantine/core'
 | 
			
		||||
 | 
			
		||||
export const header = style({
 | 
			
		||||
  paddingTop: rem(vars.spacing.sm),
 | 
			
		||||
  backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
 | 
			
		||||
  borderBottom: `${rem(1)} solid light-dark(${
 | 
			
		||||
    vars.colors.gray[2]
 | 
			
		||||
  }, transparent)`,
 | 
			
		||||
  marginBottom: rem(120),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const mainSection = style({
 | 
			
		||||
  paddingBottom: rem(vars.spacing.sm),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const user = style({
 | 
			
		||||
  color: `light-dark(${vars.colors.black}, ${vars.colors.dark[0]})`,
 | 
			
		||||
  padding: `${rem(vars.spacing.xs)} ${rem(vars.spacing.sm)}`,
 | 
			
		||||
  borderRadius: rem(vars.radius.sm),
 | 
			
		||||
  transition: 'background-color 100ms ease',
 | 
			
		||||
 | 
			
		||||
  ':hover': {
 | 
			
		||||
    backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  '@media': {
 | 
			
		||||
    [`(max-width: ${vars.breakpoints.xs})`]: {
 | 
			
		||||
      display: 'none',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const userActive = style({
 | 
			
		||||
  backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const tabsList = style({
 | 
			
		||||
  '::before': {
 | 
			
		||||
    display: 'none',
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const tab = styleVariants({
 | 
			
		||||
  default: {
 | 
			
		||||
    fontWeight: 500,
 | 
			
		||||
    height: rem(38),
 | 
			
		||||
    backgroundColor: 'transparent',
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    bottom: '-1px',
 | 
			
		||||
 | 
			
		||||
    ':hover': {
 | 
			
		||||
      backgroundColor: `light-dark(${vars.colors.gray[1]}, ${vars.colors.dark[5]})`,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  active: {
 | 
			
		||||
    backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[7]})`,
 | 
			
		||||
    borderColor: `light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[7]})`,
 | 
			
		||||
    borderBottomColor: 'transparent',
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
// import { render } from '@tests'
 | 
			
		||||
// import Header from './Header'
 | 
			
		||||
 | 
			
		||||
// describe('Header component', () => {
 | 
			
		||||
//   it('renders "Header"', () => {
 | 
			
		||||
//     render(<Header header={''} setHeader={function(): void {
 | 
			
		||||
//       throw new Error('Function not implemented.')
 | 
			
		||||
//     } } />)
 | 
			
		||||
//     // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
 | 
			
		||||
//   })
 | 
			
		||||
// })
 | 
			
		||||
| 
						 | 
				
			
			@ -1,210 +0,0 @@
 | 
			
		|||
import { logout } from '@/rtk/dispatches/auth'
 | 
			
		||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Burger,
 | 
			
		||||
  Container,
 | 
			
		||||
  Group,
 | 
			
		||||
  Menu,
 | 
			
		||||
  Tabs,
 | 
			
		||||
  Text,
 | 
			
		||||
  UnstyledButton,
 | 
			
		||||
  rem,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { MantineLogo } from '@mantine/ds'
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks'
 | 
			
		||||
import {
 | 
			
		||||
  IconChevronDown,
 | 
			
		||||
  IconLogout,
 | 
			
		||||
  IconSettings,
 | 
			
		||||
  IconSwitchHorizontal,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import cx from 'clsx'
 | 
			
		||||
import { useCallback, useState } from 'react'
 | 
			
		||||
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle'
 | 
			
		||||
import LanguagePicker from '../LanguagePicker/LanguagePicker'
 | 
			
		||||
import * as classes from './DoubleHeader.module.css.ts'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
// const user = {
 | 
			
		||||
//   name: 'Jane Spoonfighter',
 | 
			
		||||
//   email: 'janspoon@fighter.dev',
 | 
			
		||||
//   image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png',
 | 
			
		||||
// };
 | 
			
		||||
 | 
			
		||||
const tabs = ['Home', 'Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5']
 | 
			
		||||
 | 
			
		||||
const Header = ({
 | 
			
		||||
  header,
 | 
			
		||||
  setHeader,
 | 
			
		||||
}: {
 | 
			
		||||
  header: string
 | 
			
		||||
  setHeader: (newHeader: string) => void
 | 
			
		||||
}) => {
 | 
			
		||||
  const [opened, { toggle }] = useDisclosure(false)
 | 
			
		||||
  const [userMenuOpened, setUserMenuOpened] = useState(false)
 | 
			
		||||
  const dispatch = useAppDispatch()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const user = useAppSelector((state) => state.authentication.user.user)
 | 
			
		||||
 | 
			
		||||
  const items = tabs.map((tab) => (
 | 
			
		||||
    <Tabs.Tab value={tab} key={tab} onClick={() => setHeader(tab)}>
 | 
			
		||||
      {tab}
 | 
			
		||||
    </Tabs.Tab>
 | 
			
		||||
  ))
 | 
			
		||||
  const handleLogout = useCallback(() => {
 | 
			
		||||
    dispatch(logout(navigate))
 | 
			
		||||
  }, [dispatch, navigate])
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={classes.header}>
 | 
			
		||||
      <Container className={classes.mainSection} size="xl">
 | 
			
		||||
        <Group justify="space-between">
 | 
			
		||||
          <MantineLogo size={28} />
 | 
			
		||||
 | 
			
		||||
          <Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
 | 
			
		||||
 | 
			
		||||
          <Group>
 | 
			
		||||
            <LanguagePicker />
 | 
			
		||||
 | 
			
		||||
            <ColorSchemeToggle />
 | 
			
		||||
            <Menu
 | 
			
		||||
              width={260}
 | 
			
		||||
              position="bottom-end"
 | 
			
		||||
              transitionProps={{ transition: 'pop-top-right' }}
 | 
			
		||||
              onClose={() => setUserMenuOpened(false)}
 | 
			
		||||
              onOpen={() => setUserMenuOpened(true)}
 | 
			
		||||
              withinPortal
 | 
			
		||||
            >
 | 
			
		||||
              <Menu.Target>
 | 
			
		||||
                <UnstyledButton
 | 
			
		||||
                  className={cx(classes.user, {
 | 
			
		||||
                    [classes.userActive]: userMenuOpened,
 | 
			
		||||
                  })}
 | 
			
		||||
                >
 | 
			
		||||
                  <Group gap={7}>
 | 
			
		||||
                    <Avatar
 | 
			
		||||
                      src={
 | 
			
		||||
                        'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png'
 | 
			
		||||
                      }
 | 
			
		||||
                      alt={user.name}
 | 
			
		||||
                      radius="xl"
 | 
			
		||||
                      size={25}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Text fw={500} size="sm" lh={1} mr={3}>
 | 
			
		||||
                      {user.name}
 | 
			
		||||
                    </Text>
 | 
			
		||||
                    <IconChevronDown
 | 
			
		||||
                      style={{ width: rem(12), height: rem(12) }}
 | 
			
		||||
                      stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                  </Group>
 | 
			
		||||
                </UnstyledButton>
 | 
			
		||||
              </Menu.Target>
 | 
			
		||||
              <Menu.Dropdown>
 | 
			
		||||
                {/* <Menu.Item
 | 
			
		||||
                leftSection={
 | 
			
		||||
                  <IconHeart
 | 
			
		||||
                    style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                    color={theme.colors.red[6]}
 | 
			
		||||
                    stroke={1.5}
 | 
			
		||||
                  />
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                Liked posts
 | 
			
		||||
              </Menu.Item>
 | 
			
		||||
              <Menu.Item
 | 
			
		||||
                leftSection={
 | 
			
		||||
                  <IconStar
 | 
			
		||||
                    style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                    color={theme.colors.yellow[6]}
 | 
			
		||||
                    stroke={1.5}
 | 
			
		||||
                  />
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                Saved posts
 | 
			
		||||
              </Menu.Item>
 | 
			
		||||
              <Menu.Item
 | 
			
		||||
                leftSection={
 | 
			
		||||
                  <IconMessage
 | 
			
		||||
                    style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                    color={theme.colors.blue[6]}
 | 
			
		||||
                    stroke={1.5}
 | 
			
		||||
                  />
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                Your comments
 | 
			
		||||
              </Menu.Item> */}
 | 
			
		||||
 | 
			
		||||
                <Menu.Label>Settings</Menu.Label>
 | 
			
		||||
                <Menu.Item
 | 
			
		||||
                  leftSection={
 | 
			
		||||
                    <IconSettings
 | 
			
		||||
                      style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                      stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  Account settings
 | 
			
		||||
                </Menu.Item>
 | 
			
		||||
                <Menu.Item
 | 
			
		||||
                  leftSection={
 | 
			
		||||
                    <IconSwitchHorizontal
 | 
			
		||||
                      style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                      stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  Change account
 | 
			
		||||
                </Menu.Item>
 | 
			
		||||
                <Menu.Item
 | 
			
		||||
                  leftSection={
 | 
			
		||||
                    <IconLogout
 | 
			
		||||
                      style={{ width: rem(16), height: rem(16) }}
 | 
			
		||||
                      stroke={1.5}
 | 
			
		||||
                    />
 | 
			
		||||
                  }
 | 
			
		||||
                  onClick={handleLogout}
 | 
			
		||||
                >
 | 
			
		||||
                  Logout
 | 
			
		||||
                </Menu.Item>
 | 
			
		||||
 | 
			
		||||
                {/* <Menu.Divider />
 | 
			
		||||
 | 
			
		||||
              <Menu.Label>Danger zone</Menu.Label>
 | 
			
		||||
              <Menu.Item
 | 
			
		||||
                leftSection={
 | 
			
		||||
                  <IconPlayerPause style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                Pause subscription
 | 
			
		||||
              </Menu.Item>
 | 
			
		||||
              <Menu.Item
 | 
			
		||||
                color="red"
 | 
			
		||||
                leftSection={<IconTrash style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
 | 
			
		||||
              >
 | 
			
		||||
                Delete account
 | 
			
		||||
              </Menu.Item> */}
 | 
			
		||||
              </Menu.Dropdown>
 | 
			
		||||
            </Menu>
 | 
			
		||||
          </Group>
 | 
			
		||||
        </Group>
 | 
			
		||||
      </Container>
 | 
			
		||||
      <Container size="md">
 | 
			
		||||
        <Tabs
 | 
			
		||||
          defaultValue={header}
 | 
			
		||||
          variant="outline"
 | 
			
		||||
          visibleFrom="sm"
 | 
			
		||||
          classNames={{
 | 
			
		||||
            // root: classes.tab.active,
 | 
			
		||||
            list: classes.tabsList,
 | 
			
		||||
            tab: classes.tab.default,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Tabs.List justify="center">{items}</Tabs.List>
 | 
			
		||||
        </Tabs>
 | 
			
		||||
      </Container>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Header
 | 
			
		||||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import { logout } from '@/rtk/dispatches/auth'
 | 
			
		|||
import { get, post } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
			
		||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
			
		||||
import { checkPermissions } from '@/utils/checkRoles'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +27,7 @@ import {
 | 
			
		|||
  IconCalendarClock,
 | 
			
		||||
  IconChartDots2,
 | 
			
		||||
  IconDevices,
 | 
			
		||||
  IconFolders,
 | 
			
		||||
  IconLayoutSidebarLeftExpand,
 | 
			
		||||
  IconLayoutSidebarRightExpand,
 | 
			
		||||
  IconListCheck,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,13 +41,12 @@ import {
 | 
			
		|||
  IconSun,
 | 
			
		||||
  IconTicket,
 | 
			
		||||
  IconUsersGroup,
 | 
			
		||||
  IconZoomExclamation,
 | 
			
		||||
  IconZoomExclamation
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useLocation, useNavigate } from 'react-router-dom'
 | 
			
		||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
 | 
			
		||||
import classes from './NavbarSimpleColored.module.css'
 | 
			
		||||
import { checkPermissions } from '@/utils/checkRoles'
 | 
			
		||||
 | 
			
		||||
const data = [
 | 
			
		||||
  // { link: '/dashboard', label: 'Dashboard', icon: IconHome },
 | 
			
		||||
| 
						 | 
				
			
			@ -53,15 +54,15 @@ const data = [
 | 
			
		|||
    link: '/timekeeping',
 | 
			
		||||
    label: 'Timekeeping',
 | 
			
		||||
    icon: IconCalendar,
 | 
			
		||||
    permissions: 'admin,hr,staff,tester',
 | 
			
		||||
    permissions: 'admin,hr,staff,tester,accountant',
 | 
			
		||||
    group: 'staff',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    link: '/tracking',
 | 
			
		||||
    label: 'Check in/out',
 | 
			
		||||
    icon: IconScan,
 | 
			
		||||
    permissions: 'hr,admin',
 | 
			
		||||
    group: 'other',
 | 
			
		||||
    permissions: 'staff,hr,admin',
 | 
			
		||||
    group: 'staff',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    link: '/worklogs',
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +75,7 @@ const data = [
 | 
			
		|||
    link: '/leave-management',
 | 
			
		||||
    label: 'Leave Management',
 | 
			
		||||
    icon: IconCalendarClock,
 | 
			
		||||
    permissions: 'admin,hr,staff,tester',
 | 
			
		||||
    permissions: 'admin,hr,staff,tester,accountant',
 | 
			
		||||
    group: 'staff',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +141,13 @@ const data = [
 | 
			
		|||
    group: 'admin',
 | 
			
		||||
    permissions: 'admin',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    link: '/profiles',
 | 
			
		||||
    label: 'Profiles',
 | 
			
		||||
    icon: IconFolders,
 | 
			
		||||
    group: 'admin',
 | 
			
		||||
    permissions: 'admin,accountant',
 | 
			
		||||
  },
 | 
			
		||||
  // { link: '/jira', label: 'Jira', icon: IconSubtask },
 | 
			
		||||
  // { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
 | 
			
		||||
  // { link: '/general-setting', label: 'General Setting', icon: IconSettings },
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +191,7 @@ const Navbar = ({
 | 
			
		|||
    confirm_password: '',
 | 
			
		||||
  })
 | 
			
		||||
  const [countSpam, setCountSpam] = useState(0)
 | 
			
		||||
  const [avatar, setAvatar] = useState(user.user.avatar)
 | 
			
		||||
  const [avatar, setAvatar] = useState(user?.user?.avatar)
 | 
			
		||||
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const dispatch = useAppDispatch()
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +239,7 @@ const Navbar = ({
 | 
			
		|||
  // })
 | 
			
		||||
 | 
			
		||||
  const group = [
 | 
			
		||||
    { name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester' },
 | 
			
		||||
    { name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester,accountant' },
 | 
			
		||||
    { name: 'admin', label: 'Admin', permissions: 'admin' },
 | 
			
		||||
    { name: 'other', label: 'Other', permissions: 'admin,hr' },
 | 
			
		||||
    { name: 'test', label: 'Test', permissions: 'admin,tester' },
 | 
			
		||||
| 
						 | 
				
			
			@ -301,7 +309,7 @@ const Navbar = ({
 | 
			
		|||
        const res = await post(
 | 
			
		||||
          changePassword,
 | 
			
		||||
          {
 | 
			
		||||
            email: user.user.email,
 | 
			
		||||
            email: user?.user?.email,
 | 
			
		||||
            password: dataChange.password,
 | 
			
		||||
            new_password: dataChange.new_password,
 | 
			
		||||
            confirm_password: dataChange.confirm_password,
 | 
			
		||||
| 
						 | 
				
			
			@ -417,7 +425,7 @@ const Navbar = ({
 | 
			
		|||
                      cursor: 'pointer',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {user.user.name}
 | 
			
		||||
                    {user?.user?.name}
 | 
			
		||||
                  </Code>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box
 | 
			
		||||
| 
						 | 
				
			
			@ -569,7 +577,7 @@ const Navbar = ({
 | 
			
		|||
        <Box p="sm">
 | 
			
		||||
          <TextInput
 | 
			
		||||
            label="E-mail"
 | 
			
		||||
            value={user.user.email}
 | 
			
		||||
            value={user?.user?.email}
 | 
			
		||||
            disabled
 | 
			
		||||
            mb={'md'}
 | 
			
		||||
          ></TextInput>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
.root {
 | 
			
		||||
    /* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
.label[data-hovered] {
 | 
			
		||||
  background-color: var(--mantine-color-gray-0);
 | 
			
		||||
}
 | 
			
		||||
/* Add the light and dark logic using classes, for example */
 | 
			
		||||
.label[data-hovered].light {
 | 
			
		||||
  background-color: var(--mantine-color-gray-0);
 | 
			
		||||
}
 | 
			
		||||
.label[data-hovered].dark {
 | 
			
		||||
  background-color: var(--mantine-color-dark-6);
 | 
			
		||||
  color: var(--mantine-color-white);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.label[data-selected] {
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
.label[data-selected].light {
 | 
			
		||||
  background-color: var(--mantine-color-blue-0);
 | 
			
		||||
  color: var(--mantine-color-black);
 | 
			
		||||
}
 | 
			
		||||
.label[data-selected].dark {
 | 
			
		||||
  background-color: rgba(var(--mantine-color-blue-8), 0.35);
 | 
			
		||||
  color: var(--mantine-color-blue-0);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,503 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Group,
 | 
			
		||||
  Modal,
 | 
			
		||||
  RenderTreeNodePayload,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Tree,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import {
 | 
			
		||||
  IconFileTypeDocx,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFolder,
 | 
			
		||||
  IconFolderOpen,
 | 
			
		||||
  IconFolderX,
 | 
			
		||||
  IconListCheck,
 | 
			
		||||
  IconPhoto,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import classes from './AllProfiles.module.css'
 | 
			
		||||
 | 
			
		||||
interface FileIconProps {
 | 
			
		||||
  name: string
 | 
			
		||||
  isFolder: boolean
 | 
			
		||||
  expanded: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AllProfiles = () => {
 | 
			
		||||
  const [treeData, setTreeData] = useState([])
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
			
		||||
  const [currentName, setCurrentName] = useState<string>('')
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  function FileIcon({ name, isFolder, expanded }: FileIconProps) {
 | 
			
		||||
    if (name.endsWith('.pdf')) {
 | 
			
		||||
      return <IconFileTypePdf size={14} />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
			
		||||
      return <IconFileTypeDocx size={14} />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      name.endsWith('.jpg') ||
 | 
			
		||||
      name.endsWith('.png') ||
 | 
			
		||||
      name.endsWith('.jpeg') ||
 | 
			
		||||
      name.endsWith('.webp')
 | 
			
		||||
    ) {
 | 
			
		||||
      return <IconPhoto size={14} />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isFolder) {
 | 
			
		||||
      return expanded ? (
 | 
			
		||||
        <IconFolderOpen
 | 
			
		||||
          color="var(--mantine-color-yellow-9)"
 | 
			
		||||
          size={14}
 | 
			
		||||
          stroke={2.5}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <IconFolder
 | 
			
		||||
          color="var(--mantine-color-yellow-9)"
 | 
			
		||||
          size={14}
 | 
			
		||||
          stroke={2.5}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <IconFolderX color="var(--mantine-color-red-9)" size={14} stroke={2.5} />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function Leaf({
 | 
			
		||||
    node,
 | 
			
		||||
    expanded,
 | 
			
		||||
    hasChildren,
 | 
			
		||||
    elementProps,
 | 
			
		||||
  }: RenderTreeNodePayload) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Group gap={5} {...elementProps}>
 | 
			
		||||
        {!node.children ? (
 | 
			
		||||
          <a href={node.value} target="_blank">
 | 
			
		||||
            <FileIcon
 | 
			
		||||
              name={node.value}
 | 
			
		||||
              isFolder={hasChildren}
 | 
			
		||||
              expanded={expanded}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{node.label}</span>
 | 
			
		||||
          </a>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <>
 | 
			
		||||
            <FileIcon
 | 
			
		||||
              name={node.value}
 | 
			
		||||
              isFolder={hasChildren}
 | 
			
		||||
              expanded={expanded}
 | 
			
		||||
            />
 | 
			
		||||
            <span>{node.label}</span>
 | 
			
		||||
            <Tooltip label="Upload">
 | 
			
		||||
              <IconListCheck
 | 
			
		||||
                color="green"
 | 
			
		||||
                width={15}
 | 
			
		||||
                height={15}
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: node.label !== 'others' ? 'block' : 'none',
 | 
			
		||||
                  cursor: 'pointer',
 | 
			
		||||
                  zIndex: 100000,
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setCurrentName(node.label!.toString())
 | 
			
		||||
                  setOpenedProfile(true)
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
      </Group>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    formData.append('name', currentName)
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'multipart/form-data',
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        getTree()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + currentName,
 | 
			
		||||
      })
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, { file_url: url }, getAllFile)
 | 
			
		||||
      getTree()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const getTree = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles',
 | 
			
		||||
      })
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setTreeData(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getTree()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [currentName])
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          <Text>Admin/</Text>
 | 
			
		||||
          Profiles
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Box ml={'lg'}>
 | 
			
		||||
        <Tree
 | 
			
		||||
          classNames={classes}
 | 
			
		||||
          selectOnClick
 | 
			
		||||
          clearSelectionOnOutsideClick
 | 
			
		||||
          data={treeData}
 | 
			
		||||
          renderNode={(payload) => <Leaf {...payload} />}
 | 
			
		||||
        />
 | 
			
		||||
        <Modal
 | 
			
		||||
          size={'lg'}
 | 
			
		||||
          opened={openedProfile}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setOpenedProfile(false)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('cv')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label={'CV'}
 | 
			
		||||
                  placeholder="Tải lên CV"
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    0
 | 
			
		||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.doc,.docx"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CCCD</Text>
 | 
			
		||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('idCard')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="CCCD"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'none' : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên CCCD"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bảng điểm</Text>
 | 
			
		||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('transcript')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bảng điểm"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'none' : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên bảng điểm"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'flex'
 | 
			
		||||
                      : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bằng đại học</Text>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text>{`: ${
 | 
			
		||||
                      checkFileExist('universityDiploma')?.label
 | 
			
		||||
                    }`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="outline"
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    color="red"
 | 
			
		||||
                    ml={'sm'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      removeFile(checkFileExist('universityDiploma')?.value!)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bằng đại học"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'none'
 | 
			
		||||
                      : 'block',
 | 
			
		||||
                  }}
 | 
			
		||||
                  placeholder="Tải lên bằng đại học"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setUniversityDiploma(
 | 
			
		||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
			
		||||
                    )
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Text>Danh sách file khác:</Text>
 | 
			
		||||
                <Box>
 | 
			
		||||
                  {data
 | 
			
		||||
                    .find((f) => f.label === 'others')
 | 
			
		||||
                    ?.children?.map((c, index) => {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Box
 | 
			
		||||
                          key={index}
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            marginBottom: '5px',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <a href={c?.value} target="_blank">
 | 
			
		||||
                            <Text>{`${c?.label}`}</Text>
 | 
			
		||||
                          </a>
 | 
			
		||||
                          <Button
 | 
			
		||||
                            variant="outline"
 | 
			
		||||
                            size="xs"
 | 
			
		||||
                            color="red"
 | 
			
		||||
                            ml={'sm'}
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                              removeFile(c?.value!)
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            Delete
 | 
			
		||||
                          </Button>
 | 
			
		||||
                        </Box>
 | 
			
		||||
                      )
 | 
			
		||||
                    })}
 | 
			
		||||
                </Box>
 | 
			
		||||
                {otherFiles.map((fileInput, index) => (
 | 
			
		||||
                  <Group key={index}>
 | 
			
		||||
                    <FileInput
 | 
			
		||||
                      placeholder="Chọn file"
 | 
			
		||||
                      onChange={(file) =>
 | 
			
		||||
                        handleOtherFileChange(index, 'file', file!)
 | 
			
		||||
                      }
 | 
			
		||||
                      w={'30%'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <TextInput
 | 
			
		||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
			
		||||
                      value={fileInput.type}
 | 
			
		||||
                      w={'65%'}
 | 
			
		||||
                      onChange={(e) =>
 | 
			
		||||
                        handleOtherFileChange(
 | 
			
		||||
                          index,
 | 
			
		||||
                          'type',
 | 
			
		||||
                          e.currentTarget.value,
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Group>
 | 
			
		||||
                ))}
 | 
			
		||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
			
		||||
                  Thêm file khác
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button type="submit" color="blue">
 | 
			
		||||
                  Save
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AllProfiles
 | 
			
		||||
| 
						 | 
				
			
			@ -316,7 +316,7 @@ const Allocation = () => {
 | 
			
		|||
                                          <Box
 | 
			
		||||
                                            className={
 | 
			
		||||
                                              Date.now() - date.getTime() >
 | 
			
		||||
                                              172800000 * 5
 | 
			
		||||
                                              172800000 * 5 // 10 days
 | 
			
		||||
                                                ? classes['blinking-background']
 | 
			
		||||
                                                : ''
 | 
			
		||||
                                            }
 | 
			
		||||
| 
						 | 
				
			
			@ -326,7 +326,7 @@ const Allocation = () => {
 | 
			
		|||
                                              borderRadius: '10px',
 | 
			
		||||
                                              cursor: 'pointer',
 | 
			
		||||
                                              backgroundColor: issStatus
 | 
			
		||||
                                                ? issLastHistory < 172800000
 | 
			
		||||
                                                ? issLastHistory < 172800000 // 2 days
 | 
			
		||||
                                                  ? '#d1f3d1'
 | 
			
		||||
                                                  : '#ffff8a'
 | 
			
		||||
                                                : '',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { getLeaveManagement, updateNoteLeave } from '@/api/Admin'
 | 
			
		||||
import { getLeaveManagement, updateNoteLeave, exportLeaveManagement } from '@/api/Admin'
 | 
			
		||||
import { update } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { get, exportFile } from '@/rtk/helpers/apiService'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ import { notifications } from '@mantine/notifications'
 | 
			
		|||
import moment from 'moment'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
import { IconEdit } from '@tabler/icons-react'
 | 
			
		||||
import { IconEdit, IconFileExcel } from '@tabler/icons-react'
 | 
			
		||||
 | 
			
		||||
import classes from './LeaveManagement.module.css'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -245,6 +245,29 @@ const LeaveManagement = () => {
 | 
			
		|||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleExport = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const timestamp = moment().format('DDMMYYYY_HHmmss')
 | 
			
		||||
      const fileName = `LeaveManagement_${date.year}_${timestamp}.xlsx`
 | 
			
		||||
      
 | 
			
		||||
      await exportFile(
 | 
			
		||||
        exportLeaveManagement,
 | 
			
		||||
        {
 | 
			
		||||
          year: parseInt(date.year)
 | 
			
		||||
        },
 | 
			
		||||
        fileName
 | 
			
		||||
      )
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Export error:', error)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Error',
 | 
			
		||||
        message: 'Export failed',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
| 
						 | 
				
			
			@ -391,22 +414,18 @@ const LeaveManagement = () => {
 | 
			
		|||
          pl={200}
 | 
			
		||||
          style={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            // alignItems: 'end',
 | 
			
		||||
            justifyContent: 'end',
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box display={'flex'} style={{ alignItems: 'end' }}>
 | 
			
		||||
            {/* <Tooltip label="Save working days">
 | 
			
		||||
            <Button
 | 
			
		||||
              size="xs"
 | 
			
		||||
              ml={'sm'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  //form add user new
 | 
			
		||||
                }}
 | 
			
		||||
              onClick={handleExport}
 | 
			
		||||
              leftSection={<IconFileExcel size={16} />}
 | 
			
		||||
            >
 | 
			
		||||
                Add
 | 
			
		||||
              Export Excel
 | 
			
		||||
            </Button>
 | 
			
		||||
            </Tooltip> */}
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,34 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  getProfilesData,
 | 
			
		||||
  listUserTechnical,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
  updateProfilesData,
 | 
			
		||||
  updateUserTechnical,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { changePassword } from '@/api/Auth'
 | 
			
		||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
			
		||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
			
		||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
			
		||||
import { logout } from '@/rtk/dispatches/auth'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
			
		||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
			
		||||
import { getUser } from '@/rtk/localStorage'
 | 
			
		||||
import { getAccessToken, getUser } from '@/rtk/localStorage'
 | 
			
		||||
import { success } from '@/rtk/slices/auth'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Flex,
 | 
			
		||||
  Group,
 | 
			
		||||
  Loader,
 | 
			
		||||
  Modal,
 | 
			
		||||
  PasswordInput,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Title,
 | 
			
		||||
| 
						 | 
				
			
			@ -28,18 +36,25 @@ import {
 | 
			
		|||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconExchange,
 | 
			
		||||
  IconFolder,
 | 
			
		||||
  IconPasswordUser,
 | 
			
		||||
  IconUserCode,
 | 
			
		||||
  IconUserCog,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import classes from './Profile.module.css'
 | 
			
		||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
 | 
			
		||||
const isCompactMenu = false
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
}
 | 
			
		||||
const Profile = () => {
 | 
			
		||||
  const user = useAppSelector((state) => state.authentication.user)
 | 
			
		||||
  const userData = getUser()
 | 
			
		||||
| 
						 | 
				
			
			@ -122,12 +137,102 @@ const Profile = () => {
 | 
			
		|||
    return []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'multipart/form-data',
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      
 | 
			
		||||
      if(response.status === 200){
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
			
		||||
      })
 | 
			
		||||
      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)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      const result = await getListProfilesData()
 | 
			
		||||
      setDataProfile(result ?? [])
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleChangePassword = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +306,24 @@ const Profile = () => {
 | 
			
		|||
    dispatch(logout(navigate))
 | 
			
		||||
  }, [dispatch, navigate])
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
 | 
			
		||||
      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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
| 
						 | 
				
			
			@ -329,6 +452,15 @@ const Profile = () => {
 | 
			
		|||
                  Your Technical
 | 
			
		||||
                </Button>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <Button
 | 
			
		||||
                style={{ width: '50%' }}
 | 
			
		||||
                color="orange"
 | 
			
		||||
                onClick={() => setOpenedProfile(true)}
 | 
			
		||||
              >
 | 
			
		||||
                <IconFolder stroke={1.5} />
 | 
			
		||||
                Update profile
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Box>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -437,6 +569,173 @@ const Profile = () => {
 | 
			
		|||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
 | 
			
		||||
        <Modal
 | 
			
		||||
          size={'lg'}
 | 
			
		||||
          opened={openedProfile}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setOpenedProfile(false)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('cv')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label={'CV'}
 | 
			
		||||
                  placeholder="Tải lên CV"
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    0
 | 
			
		||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.doc,.docx"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CCCD</Text>
 | 
			
		||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('idCard')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="CCCD"
 | 
			
		||||
                  style={{ display: checkFileExist('idCard') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên CCCD"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
			
		||||
                />
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bảng điểm</Text>
 | 
			
		||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
			
		||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('transcript')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bảng điểm"
 | 
			
		||||
                  style={{ display: checkFileExist('transcript') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên bảng điểm"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: checkFileExist('universityDiploma')
 | 
			
		||||
                      ? 'flex'
 | 
			
		||||
                      : 'none',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>Bằng đại học</Text>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                  >
 | 
			
		||||
                    <Text>{`: ${
 | 
			
		||||
                      checkFileExist('universityDiploma')?.label
 | 
			
		||||
                    }`}</Text>
 | 
			
		||||
                  </a>
 | 
			
		||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('universityDiploma')?.value!)}}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Box>
 | 
			
		||||
                <FileInput
 | 
			
		||||
                  label="Bằng đại học"
 | 
			
		||||
                  style={{ display: checkFileExist('universityDiploma') ? 'none' : 'block' }}
 | 
			
		||||
                  placeholder="Tải lên bằng đại học"
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
                    setUniversityDiploma(
 | 
			
		||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
			
		||||
                    )
 | 
			
		||||
                  }}
 | 
			
		||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Text>Danh sách file khác:</Text>
 | 
			
		||||
                  <Box>
 | 
			
		||||
                    {data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{
 | 
			
		||||
                      return <Box key={index}
 | 
			
		||||
                      style={{
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        marginBottom:'5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={c?.value}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                      >
 | 
			
		||||
                        <Text>{`${
 | 
			
		||||
                          c?.label
 | 
			
		||||
                        }`}</Text>
 | 
			
		||||
                      </a>
 | 
			
		||||
                      <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(c?.value!)}}>
 | 
			
		||||
                        Delete
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                    })}
 | 
			
		||||
                  </Box>
 | 
			
		||||
                {otherFiles.map((fileInput, index) => (
 | 
			
		||||
                  <Group key={index}>
 | 
			
		||||
                    <FileInput
 | 
			
		||||
                      placeholder="Chọn file"
 | 
			
		||||
                      onChange={(file) =>
 | 
			
		||||
                        handleOtherFileChange(index, 'file', file!)
 | 
			
		||||
                      }
 | 
			
		||||
                      w={'30%'}
 | 
			
		||||
                    />
 | 
			
		||||
                    <TextInput
 | 
			
		||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
			
		||||
                      value={fileInput.type}
 | 
			
		||||
                      w={'65%'}
 | 
			
		||||
                      onChange={(e) =>
 | 
			
		||||
                        handleOtherFileChange(
 | 
			
		||||
                          index,
 | 
			
		||||
                          'type',
 | 
			
		||||
                          e.currentTarget.value,
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Group>
 | 
			
		||||
                ))}
 | 
			
		||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
			
		||||
                  Thêm file khác
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button type="submit" color="blue">
 | 
			
		||||
                  Save
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Stack>
 | 
			
		||||
            </form>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -509,7 +509,7 @@ const TicketsManagement = () => {
 | 
			
		|||
 | 
			
		||||
            <Textarea
 | 
			
		||||
              label="Admin Notes"
 | 
			
		||||
              required
 | 
			
		||||
              // required
 | 
			
		||||
              value={form.values.admin_note}
 | 
			
		||||
              onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
 | 
			
		||||
            />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteNote,
 | 
			
		||||
  exportTimekeeping,
 | 
			
		||||
  getListMaster,
 | 
			
		||||
  getTheTimesheet,
 | 
			
		||||
  updateMultipleUserWorkingTime,
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +8,7 @@ import {
 | 
			
		|||
  updateWorkingDays,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { update, Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { exportFile, get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +32,7 @@ import {
 | 
			
		|||
  IconPointFilled,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
  IconX,
 | 
			
		||||
  IconFileExcel,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
| 
						 | 
				
			
			@ -435,12 +437,29 @@ const Timekeeping = () => {
 | 
			
		|||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleExport = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const timestamp = moment().format('DDMMYYYY_HHmmss')
 | 
			
		||||
      const fileName = `Timekeeping_${date.month}_${date.year}_${timestamp}.xlsx`
 | 
			
		||||
      
 | 
			
		||||
      await exportFile(
 | 
			
		||||
        exportTimekeeping,
 | 
			
		||||
        {
 | 
			
		||||
          month: date.month,
 | 
			
		||||
          year: date.year,
 | 
			
		||||
          working_days: workingDays
 | 
			
		||||
        },
 | 
			
		||||
        fileName
 | 
			
		||||
      )
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Export error:', error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          Timekeeping
 | 
			
		||||
        </h3>
 | 
			
		||||
        <h3>Timekeeping</h3>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Drawer
 | 
			
		||||
        opened={opened1}
 | 
			
		||||
| 
						 | 
				
			
			@ -511,7 +530,6 @@ const Timekeeping = () => {
 | 
			
		|||
          <span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
 | 
			
		||||
          <span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <Select
 | 
			
		||||
          mb={'md'}
 | 
			
		||||
          searchable
 | 
			
		||||
| 
						 | 
				
			
			@ -688,6 +706,14 @@ const Timekeeping = () => {
 | 
			
		|||
                Save
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
            <Button 
 | 
			
		||||
              onClick={handleExport} 
 | 
			
		||||
              size="xs" 
 | 
			
		||||
              ml="xl"
 | 
			
		||||
              leftSection={<IconFileExcel size={16} />}
 | 
			
		||||
            >
 | 
			
		||||
              Export Excel
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Box
 | 
			
		||||
| 
						 | 
				
			
			@ -866,13 +892,22 @@ const Timekeeping = () => {
 | 
			
		|||
                      // offset={{ mainAxis: 5, crossAxis: 0 }}
 | 
			
		||||
                      label={showTooltipAllNote(user)}
 | 
			
		||||
                    >
 | 
			
		||||
                      <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 +
 | 
			
		||||
                                'storage/' +
 | 
			
		||||
                                user.user.avatar
 | 
			
		||||
                              : import.meta.env.VITE_BACKEND_URL +
 | 
			
		||||
                                'image/storage/' +
 | 
			
		||||
                           user.user.avatar}/>{user.user.name}</div>
 | 
			
		||||
                                user.user.avatar
 | 
			
		||||
                          }
 | 
			
		||||
                        />
 | 
			
		||||
                        {user.user.name}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                  </Table.Td>
 | 
			
		||||
                  <Table.Td ta={'center'}>{totalDays}</Table.Td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -209,7 +209,7 @@ const UsersManagement = () => {
 | 
			
		|||
            <MultiSelect
 | 
			
		||||
              label={'Permission(s)'}
 | 
			
		||||
              required
 | 
			
		||||
              data={['staff', 'admin', 'hr', 'tester']}
 | 
			
		||||
              data={['staff', 'admin', 'hr', 'tester', 'accountant']}
 | 
			
		||||
              value={
 | 
			
		||||
                typeof form.values.permission === 'string'
 | 
			
		||||
                  ? form.values.permission
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
import ResetPassword from '@/components/Authentication/ResetPassword'
 | 
			
		||||
import BasePage from '@/components/BasePage/BasePage'
 | 
			
		||||
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
 | 
			
		||||
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
 | 
			
		||||
import Allocation from '@/pages/Allocation/Allocation'
 | 
			
		||||
import PageLogin from '@/pages/Auth/Login/Login'
 | 
			
		||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +84,7 @@ const mainRoutes = [
 | 
			
		|||
  {
 | 
			
		||||
    path: '/timekeeping',
 | 
			
		||||
    element: (
 | 
			
		||||
      <ProtectedRoute mode="home" permission="staff">
 | 
			
		||||
      <ProtectedRoute mode="home" permission="staff,accountant">
 | 
			
		||||
        <BasePage
 | 
			
		||||
          main={
 | 
			
		||||
            <>
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +98,7 @@ const mainRoutes = [
 | 
			
		|||
  {
 | 
			
		||||
    path: '/leave-management',
 | 
			
		||||
    element: (
 | 
			
		||||
      <ProtectedRoute mode="home" permission="staff">
 | 
			
		||||
      <ProtectedRoute mode="home" permission="staff,accountant">
 | 
			
		||||
        <BasePage
 | 
			
		||||
          main={
 | 
			
		||||
            <>
 | 
			
		||||
| 
						 | 
				
			
			@ -234,6 +235,20 @@ const mainRoutes = [
 | 
			
		|||
      </ProtectedRoute>
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/profiles',
 | 
			
		||||
    element: (
 | 
			
		||||
      <ProtectedRoute mode="route" permission="admin,accountant">
 | 
			
		||||
        <BasePage
 | 
			
		||||
          main={
 | 
			
		||||
            <>
 | 
			
		||||
              <AllProfiles />
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
        ></BasePage>
 | 
			
		||||
      </ProtectedRoute>
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  // {
 | 
			
		||||
  //   path: '/packages',
 | 
			
		||||
  //   element: (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import { getFormDataHeader, getHeaderInfo } from '@/rtk/helpers/tokenCreator'
 | 
			
		|||
import { removeTokens } from '@/rtk/localStorage'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
const handleResponse = (response: any) => {
 | 
			
		||||
  if (response.status === 401) {
 | 
			
		||||
    removeTokens()
 | 
			
		||||
| 
						 | 
				
			
			@ -156,3 +157,45 @@ export const postImage = async (url: string, body: any, method: any) => {
 | 
			
		|||
    throw handleResponse(err.response)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const exportFile = async (url: string, params: any = {}, fileName: string) => {
 | 
			
		||||
  const header = await getHeaderInfo()
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios.get(url, {
 | 
			
		||||
      ...header,
 | 
			
		||||
      params,
 | 
			
		||||
      responseType: 'blob'
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (response.status === 200) {
 | 
			
		||||
      const blob = new Blob([response.data])
 | 
			
		||||
      const downloadUrl = window.URL.createObjectURL(blob)
 | 
			
		||||
      const link = document.createElement('a')
 | 
			
		||||
      link.href = downloadUrl
 | 
			
		||||
      link.download = fileName
 | 
			
		||||
      
 | 
			
		||||
      document.body.appendChild(link)
 | 
			
		||||
      link.click()
 | 
			
		||||
      window.URL.revokeObjectURL(downloadUrl)
 | 
			
		||||
      document.body.removeChild(link)
 | 
			
		||||
 | 
			
		||||
      // notifications.show({
 | 
			
		||||
      //   title: 'Success',
 | 
			
		||||
      //   message: 'Export successfully',
 | 
			
		||||
      //   color: 'green',
 | 
			
		||||
      // })
 | 
			
		||||
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    throw new Error('Export failed')
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    notifications.show({
 | 
			
		||||
      title: 'Error',
 | 
			
		||||
      message: error.message || 'Export failed',
 | 
			
		||||
      color: 'red',
 | 
			
		||||
    })
 | 
			
		||||
    throw error
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue