Merge pull request 'truong-report-excel' (#98) from truong-report-excel into dev
Reviewed-on: #98
This commit is contained in:
commit
cfba5175a6
|
|
@ -3,7 +3,9 @@
|
||||||
namespace Modules\Admin\app\Http\Controllers;
|
namespace Modules\Admin\app\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\WarningLongTask;
|
||||||
use App\Mail\WorklogReport;
|
use App\Mail\WorklogReport;
|
||||||
|
use App\Models\User;
|
||||||
use App\Traits\HasFilterRequest;
|
use App\Traits\HasFilterRequest;
|
||||||
use App\Traits\HasOrderByRequest;
|
use App\Traits\HasOrderByRequest;
|
||||||
use App\Traits\HasSearchRequest;
|
use App\Traits\HasSearchRequest;
|
||||||
|
|
@ -183,8 +185,8 @@ class JiraController extends Controller
|
||||||
$workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
|
$workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
|
||||||
|
|
||||||
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
|
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
|
||||||
Mail::to(['luanlt632000@gmail.com'])->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));
|
Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
|
||||||
|
|
||||||
// return "Email sent successfully!";
|
// return "Email sent successfully!";
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -248,7 +250,6 @@ class JiraController extends Controller
|
||||||
return $tasksByUser;
|
return $tasksByUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function getAllUserDoing(Request $request)
|
public function getAllUserDoing(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -303,6 +304,7 @@ class JiraController extends Controller
|
||||||
'status' => true
|
'status' => true
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAllIssueByIdSprint(Request $request)
|
public function getAllIssueByIdSprint(Request $request)
|
||||||
{
|
{
|
||||||
$id = $request->input('id');
|
$id = $request->input('id');
|
||||||
|
|
@ -313,4 +315,26 @@ class JiraController extends Controller
|
||||||
'status' => true
|
'status' => true
|
||||||
], 200);
|
], 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;
|
namespace Modules\Admin\app\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exports\LeaveManagementExport;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\InitializeLeaveDays;
|
use App\Jobs\InitializeLeaveDays;
|
||||||
use App\Models\LeaveDays;
|
use App\Models\LeaveDays;
|
||||||
|
|
@ -9,6 +10,7 @@ use App\Models\Notes;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
class LeaveManagementController extends Controller
|
class LeaveManagementController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -139,4 +141,20 @@ class LeaveManagementController extends Controller
|
||||||
|
|
||||||
return response()->json(['status' => true, 'message' => 'Updated successfully']);
|
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();
|
$user->save();
|
||||||
return AbstractController::ResultSuccess($path);
|
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 = [
|
$rules = [
|
||||||
'ticket_id' => 'required',
|
'ticket_id' => 'required',
|
||||||
'action' => 'required',
|
'action' => 'required',
|
||||||
'admin_note' => 'required'
|
// 'admin_note' => 'required'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Validate the request
|
// Validate the request
|
||||||
|
|
@ -344,13 +344,13 @@ class TicketController extends Controller
|
||||||
$date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
|
$date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
|
||||||
|
|
||||||
//Default: ALL
|
//Default: ALL
|
||||||
$start = $date->copy()->setTime(7, 31, 11);
|
$start = $date->copy()->setTime(7, 30, 0);
|
||||||
$end = $date->copy()->setTime(17, 1, 11);
|
$end = $date->copy()->setTime(17, 0, 0);
|
||||||
|
|
||||||
if ($type == 'S') {
|
if ($type == 'S') {
|
||||||
$end = $date->copy()->setTime(11, 31, 11);
|
$end = $date->copy()->setTime(11, 30, 0);
|
||||||
} else if ($type == 'C') {
|
} else if ($type == 'C') {
|
||||||
$start = $date->copy()->setTime(11, 31, 11);
|
$start = $date->copy()->setTime(11, 30, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tracking::insert([
|
Tracking::insert([
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ use Illuminate\Support\Facades\DB;
|
||||||
use Modules\Admin\app\Models\Admin;
|
use Modules\Admin\app\Models\Admin;
|
||||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
||||||
use Modules\Admin\app\Models\Tracking;
|
use Modules\Admin\app\Models\Tracking;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use App\Exports\TimekeepingExport;
|
||||||
|
|
||||||
class TimekeepingController extends Controller
|
class TimekeepingController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -185,4 +187,38 @@ class TimekeepingController extends Controller
|
||||||
|
|
||||||
return response()->json(['message' => 'Delete fail', 'status' => false]);
|
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;
|
namespace Modules\Admin\app\Http\Controllers;
|
||||||
|
|
||||||
use App\Helper\Cache\CurrentMonthTimekeeping;
|
use App\Events\WarningChekinLate;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Mail\CheckinLateMail;
|
||||||
use App\Traits\AnalyzeData;
|
use App\Traits\AnalyzeData;
|
||||||
use App\Traits\HasFilterRequest;
|
use App\Traits\HasFilterRequest;
|
||||||
use App\Traits\HasOrderByRequest;
|
use App\Traits\HasOrderByRequest;
|
||||||
|
|
@ -11,6 +12,7 @@ use App\Traits\HasSearchRequest;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Admin\app\Models\Admin;
|
use Modules\Admin\app\Models\Admin;
|
||||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
||||||
|
|
@ -163,14 +165,9 @@ class TrackingController extends Controller
|
||||||
$id = $request->get('id');
|
$id = $request->get('id');
|
||||||
|
|
||||||
$tracking = Tracking::find($id);
|
$tracking = Tracking::find($id);
|
||||||
// $id = $request->get('id');
|
|
||||||
|
|
||||||
// $tracking = Tracking::find($id);
|
$user = Admin::find($tracking->user_id);
|
||||||
// $payload = $request->all();
|
$this->sendCheckinLateMail($user, $tracking->created_at, $tracking->status);
|
||||||
|
|
||||||
// if ($tracking) {
|
|
||||||
// $tracking->update($payload);
|
|
||||||
// }
|
|
||||||
if ($request->hasFile('image')) {
|
if ($request->hasFile('image')) {
|
||||||
$file = $request->file('image');
|
$file = $request->file('image');
|
||||||
$filename = $request->file_name;
|
$filename = $request->file_name;
|
||||||
|
|
@ -203,4 +200,54 @@ class TrackingController extends Controller
|
||||||
'status' => true
|
'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([
|
Route::group([
|
||||||
'prefix' => 'timekeeping',
|
'prefix' => 'timekeeping',
|
||||||
], function () {
|
], 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('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
|
||||||
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->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::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-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
|
||||||
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
|
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([
|
Route::group([
|
||||||
|
|
@ -145,7 +146,8 @@ Route::middleware('api')
|
||||||
Route::group([
|
Route::group([
|
||||||
'prefix' => 'leave-management',
|
'prefix' => 'leave-management',
|
||||||
], function () {
|
], 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');
|
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::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([
|
Route::group([
|
||||||
'prefix' => 'criterias',
|
'prefix' => 'criterias',
|
||||||
], function () {
|
], function () {
|
||||||
|
|
@ -212,4 +222,5 @@ Route::group([
|
||||||
'prefix' => 'v1/admin/jira',
|
'prefix' => 'v1/admin/jira',
|
||||||
], function () {
|
], function () {
|
||||||
Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
|
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
|
// Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
|
||||||
$schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
|
$schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
|
||||||
$schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
|
$schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
|
||||||
|
|
||||||
|
// Chạy buổi sáng lúc 12:00
|
||||||
|
$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 {
|
} 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)
|
LeaveDays::where('ld_year', $this->year)
|
||||||
->where('ld_user_id', $user->id)
|
->where('ld_user_id', $user->id)
|
||||||
->update([
|
->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) {
|
foreach ($issues['issues'] as $issue) {
|
||||||
$projectName = $issue['fields']['project']['name'];
|
$projectName = $issue['fields']['project']['name'];
|
||||||
$username = $issue['fields']['assignee']['displayName'];
|
$username = $issue['fields']['assignee']['displayName'];
|
||||||
|
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
||||||
if (!isset($groupedIssues[$projectName])) {
|
if (!isset($groupedIssues[$projectName])) {
|
||||||
$groupedIssues[$projectName] = [];
|
$groupedIssues[$projectName] = [];
|
||||||
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
|
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
|
||||||
|
|
||||||
'allowed_methods' => ['*'],
|
'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"
|
"test": "npm run jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@mantine/charts": "^7.4.1",
|
"@mantine/core": "^7.13.2",
|
||||||
"@mantine/core": "7.2.1",
|
"@mantine/dates": "^7.13.2",
|
||||||
"@mantine/dates": "^7.3.2",
|
"@mantine/form": "^7.13.2",
|
||||||
"@mantine/ds": "^7.2.2",
|
"@mantine/hooks": "^7.13.2",
|
||||||
"@mantine/form": "^7.2.2",
|
"@mantine/notifications": "^7.13.2",
|
||||||
"@mantine/hooks": "7.2.1",
|
"@mantine/vanilla-extract": "^7.13.2",
|
||||||
"@mantine/notifications": "^7.2.2",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
"@mantine/vanilla-extract": "7.2.1",
|
"@tabler/icons-react": "^3.19.0",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@uiw/react-codemirror": "^4.23.5",
|
||||||
"@tabler/icons-react": "^2.40.0",
|
"@vanilla-extract/css": "^1.16.0",
|
||||||
"@uiw/react-codemirror": "^4.21.21",
|
|
||||||
"@vanilla-extract/css": "^1.13.0",
|
|
||||||
"axios": "^1.6.1",
|
"axios": "^1.6.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
|
@ -33,6 +31,7 @@
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-doc-viewer": "^0.1.14",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
|
||||||
|
|
||||||
//Timekeeping
|
//Timekeeping
|
||||||
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
|
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
|
||||||
|
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
|
||||||
export const updateMultipleUserWorkingTime =
|
export const updateMultipleUserWorkingTime =
|
||||||
API_URL + 'v1/admin/timekeeping/addMutilple'
|
API_URL + 'v1/admin/timekeeping/addMutilple'
|
||||||
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
|
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 getLeaveManagement = API_URL + 'v1/admin/leave-management'
|
||||||
export const updateNoteLeave =
|
export const updateNoteLeave =
|
||||||
API_URL + 'v1/admin/leave-management/saveNoteLeave'
|
API_URL + 'v1/admin/leave-management/saveNoteLeave'
|
||||||
|
export const exportLeaveManagement = API_URL + 'v1/admin/leave-management/export'
|
||||||
|
|
||||||
//Tickets
|
//Tickets
|
||||||
export const getTickets = API_URL + 'v1/admin/ticket/all'
|
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 evaluation = API_URL + 'v1/admin/evaluation/report'
|
||||||
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
|
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
|
//Technical
|
||||||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
||||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
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 { get, post } from '@/rtk/helpers/apiService'
|
||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||||
|
import { checkPermissions } from '@/utils/checkRoles'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -26,6 +27,7 @@ import {
|
||||||
IconCalendarClock,
|
IconCalendarClock,
|
||||||
IconChartDots2,
|
IconChartDots2,
|
||||||
IconDevices,
|
IconDevices,
|
||||||
|
IconFolders,
|
||||||
IconLayoutSidebarLeftExpand,
|
IconLayoutSidebarLeftExpand,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand,
|
||||||
IconListCheck,
|
IconListCheck,
|
||||||
|
|
@ -39,13 +41,12 @@ import {
|
||||||
IconSun,
|
IconSun,
|
||||||
IconTicket,
|
IconTicket,
|
||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
IconZoomExclamation,
|
IconZoomExclamation
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||||
import classes from './NavbarSimpleColored.module.css'
|
import classes from './NavbarSimpleColored.module.css'
|
||||||
import { checkPermissions } from '@/utils/checkRoles'
|
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
||||||
|
|
@ -53,15 +54,15 @@ const data = [
|
||||||
link: '/timekeeping',
|
link: '/timekeeping',
|
||||||
label: 'Timekeeping',
|
label: 'Timekeeping',
|
||||||
icon: IconCalendar,
|
icon: IconCalendar,
|
||||||
permissions: 'admin,hr,staff,tester',
|
permissions: 'admin,hr,staff,tester,accountant',
|
||||||
group: 'staff',
|
group: 'staff',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: '/tracking',
|
link: '/tracking',
|
||||||
label: 'Check in/out',
|
label: 'Check in/out',
|
||||||
icon: IconScan,
|
icon: IconScan,
|
||||||
permissions: 'hr,admin',
|
permissions: 'staff,hr,admin',
|
||||||
group: 'other',
|
group: 'staff',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: '/worklogs',
|
link: '/worklogs',
|
||||||
|
|
@ -74,7 +75,7 @@ const data = [
|
||||||
link: '/leave-management',
|
link: '/leave-management',
|
||||||
label: 'Leave Management',
|
label: 'Leave Management',
|
||||||
icon: IconCalendarClock,
|
icon: IconCalendarClock,
|
||||||
permissions: 'admin,hr,staff,tester',
|
permissions: 'admin,hr,staff,tester,accountant',
|
||||||
group: 'staff',
|
group: 'staff',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -140,6 +141,13 @@ const data = [
|
||||||
group: 'admin',
|
group: 'admin',
|
||||||
permissions: 'admin',
|
permissions: 'admin',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: '/profiles',
|
||||||
|
label: 'Profiles',
|
||||||
|
icon: IconFolders,
|
||||||
|
group: 'admin',
|
||||||
|
permissions: 'admin,accountant',
|
||||||
|
},
|
||||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||||
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
||||||
|
|
@ -183,7 +191,7 @@ const Navbar = ({
|
||||||
confirm_password: '',
|
confirm_password: '',
|
||||||
})
|
})
|
||||||
const [countSpam, setCountSpam] = useState(0)
|
const [countSpam, setCountSpam] = useState(0)
|
||||||
const [avatar, setAvatar] = useState(user.user.avatar)
|
const [avatar, setAvatar] = useState(user?.user?.avatar)
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
@ -231,7 +239,7 @@ const Navbar = ({
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const group = [
|
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: 'admin', label: 'Admin', permissions: 'admin' },
|
||||||
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
|
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
|
||||||
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
||||||
|
|
@ -301,7 +309,7 @@ const Navbar = ({
|
||||||
const res = await post(
|
const res = await post(
|
||||||
changePassword,
|
changePassword,
|
||||||
{
|
{
|
||||||
email: user.user.email,
|
email: user?.user?.email,
|
||||||
password: dataChange.password,
|
password: dataChange.password,
|
||||||
new_password: dataChange.new_password,
|
new_password: dataChange.new_password,
|
||||||
confirm_password: dataChange.confirm_password,
|
confirm_password: dataChange.confirm_password,
|
||||||
|
|
@ -417,7 +425,7 @@ const Navbar = ({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.user.name}
|
{user?.user?.name}
|
||||||
</Code>
|
</Code>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -569,7 +577,7 @@ const Navbar = ({
|
||||||
<Box p="sm">
|
<Box p="sm">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="E-mail"
|
label="E-mail"
|
||||||
value={user.user.email}
|
value={user?.user?.email}
|
||||||
disabled
|
disabled
|
||||||
mb={'md'}
|
mb={'md'}
|
||||||
></TextInput>
|
></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
|
<Box
|
||||||
className={
|
className={
|
||||||
Date.now() - date.getTime() >
|
Date.now() - date.getTime() >
|
||||||
172800000 * 5
|
172800000 * 5 // 10 days
|
||||||
? classes['blinking-background']
|
? classes['blinking-background']
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +326,7 @@ const Allocation = () => {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: issStatus
|
backgroundColor: issStatus
|
||||||
? issLastHistory < 172800000
|
? issLastHistory < 172800000 // 2 days
|
||||||
? '#d1f3d1'
|
? '#d1f3d1'
|
||||||
: '#ffff8a'
|
: '#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 { update } from '@/rtk/helpers/CRUD'
|
||||||
import { get } from '@/rtk/helpers/apiService'
|
import { get, exportFile } from '@/rtk/helpers/apiService'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -20,7 +20,7 @@ import { notifications } from '@mantine/notifications'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { IconEdit } from '@tabler/icons-react'
|
import { IconEdit, IconFileExcel } from '@tabler/icons-react'
|
||||||
|
|
||||||
import classes from './LeaveManagement.module.css'
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
|
|
@ -391,22 +414,18 @@ const LeaveManagement = () => {
|
||||||
pl={200}
|
pl={200}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
// alignItems: 'end',
|
|
||||||
justifyContent: 'end',
|
justifyContent: 'end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box display={'flex'} style={{ alignItems: 'end' }}>
|
<Box display={'flex'} style={{ alignItems: 'end' }}>
|
||||||
{/* <Tooltip label="Save working days">
|
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
ml={'sm'}
|
ml={'sm'}
|
||||||
onClick={() => {
|
onClick={handleExport}
|
||||||
//form add user new
|
leftSection={<IconFileExcel size={16} />}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Add
|
Export Excel
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip> */}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
import {
|
import {
|
||||||
|
deleteFile,
|
||||||
|
getAllFilesInProfiles,
|
||||||
getProfilesData,
|
getProfilesData,
|
||||||
listUserTechnical,
|
listUserTechnical,
|
||||||
|
updateProfileFolder,
|
||||||
updateProfilesData,
|
updateProfilesData,
|
||||||
updateUserTechnical,
|
updateUserTechnical,
|
||||||
} from '@/api/Admin'
|
} from '@/api/Admin'
|
||||||
import { changePassword } from '@/api/Auth'
|
import { changePassword } from '@/api/Auth'
|
||||||
|
import DataTableAll from '@/components/DataTable/DataTable'
|
||||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
||||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
||||||
import { logout } from '@/rtk/dispatches/auth'
|
import { logout } from '@/rtk/dispatches/auth'
|
||||||
|
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
|
import { get, post, postImage } from '@/rtk/helpers/apiService'
|
||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||||
import { getUser } from '@/rtk/localStorage'
|
import { getAccessToken, getUser } from '@/rtk/localStorage'
|
||||||
import { success } from '@/rtk/slices/auth'
|
import { success } from '@/rtk/slices/auth'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
FileInput,
|
||||||
Flex,
|
Flex,
|
||||||
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
|
|
@ -28,18 +36,25 @@ import {
|
||||||
import { notifications } from '@mantine/notifications'
|
import { notifications } from '@mantine/notifications'
|
||||||
import {
|
import {
|
||||||
IconExchange,
|
IconExchange,
|
||||||
|
IconFolder,
|
||||||
IconPasswordUser,
|
IconPasswordUser,
|
||||||
IconUserCode,
|
IconUserCode,
|
||||||
IconUserCog,
|
IconUserCog,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import moment from 'moment'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import classes from './Profile.module.css'
|
import classes from './Profile.module.css'
|
||||||
import DataTableAll from '@/components/DataTable/DataTable'
|
|
||||||
import moment from 'moment'
|
|
||||||
|
|
||||||
const isCompactMenu = false
|
const isCompactMenu = false
|
||||||
|
|
||||||
|
type TFileProfile = {
|
||||||
|
label: string
|
||||||
|
type: string
|
||||||
|
value: string
|
||||||
|
children?: TFileProfile[]
|
||||||
|
}
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const user = useAppSelector((state) => state.authentication.user)
|
const user = useAppSelector((state) => state.authentication.user)
|
||||||
const userData = getUser()
|
const userData = getUser()
|
||||||
|
|
@ -122,12 +137,102 @@ const Profile = () => {
|
||||||
return []
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const result = await getListProfilesData()
|
const result = await getListProfilesData()
|
||||||
setDataProfile(result ?? [])
|
setDataProfile(result ?? [])
|
||||||
}
|
}
|
||||||
fetchData()
|
fetchData()
|
||||||
|
getAllFile()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleChangePassword = async () => {
|
const handleChangePassword = async () => {
|
||||||
|
|
@ -201,6 +306,24 @@ const Profile = () => {
|
||||||
dispatch(logout(navigate))
|
dispatch(logout(navigate))
|
||||||
}, [dispatch, 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
|
|
@ -329,6 +452,15 @@ const Profile = () => {
|
||||||
Your Technical
|
Your Technical
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style={{ width: '50%' }}
|
||||||
|
color="orange"
|
||||||
|
onClick={() => setOpenedProfile(true)}
|
||||||
|
>
|
||||||
|
<IconFolder stroke={1.5} />
|
||||||
|
Update profile
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -437,6 +569,173 @@ const Profile = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</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>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ const TicketsManagement = () => {
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Admin Notes"
|
label="Admin Notes"
|
||||||
required
|
// required
|
||||||
value={form.values.admin_note}
|
value={form.values.admin_note}
|
||||||
onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
|
onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
deleteNote,
|
deleteNote,
|
||||||
|
exportTimekeeping,
|
||||||
getListMaster,
|
getListMaster,
|
||||||
getTheTimesheet,
|
getTheTimesheet,
|
||||||
updateMultipleUserWorkingTime,
|
updateMultipleUserWorkingTime,
|
||||||
|
|
@ -7,7 +8,7 @@ import {
|
||||||
updateWorkingDays,
|
updateWorkingDays,
|
||||||
} from '@/api/Admin'
|
} from '@/api/Admin'
|
||||||
import { update, Xdelete } from '@/rtk/helpers/CRUD'
|
import { update, Xdelete } from '@/rtk/helpers/CRUD'
|
||||||
import { get } from '@/rtk/helpers/apiService'
|
import { exportFile, get } from '@/rtk/helpers/apiService'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -31,6 +32,7 @@ import {
|
||||||
IconPointFilled,
|
IconPointFilled,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconX,
|
IconX,
|
||||||
|
IconFileExcel,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
<h3>
|
<h3>Timekeeping</h3>
|
||||||
Timekeeping
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<Drawer
|
<Drawer
|
||||||
opened={opened1}
|
opened={opened1}
|
||||||
|
|
@ -511,7 +530,6 @@ const Timekeeping = () => {
|
||||||
<span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
|
<span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
|
||||||
<span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
|
<span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
mb={'md'}
|
mb={'md'}
|
||||||
searchable
|
searchable
|
||||||
|
|
@ -688,6 +706,14 @@ const Timekeeping = () => {
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
onClick={handleExport}
|
||||||
|
size="xs"
|
||||||
|
ml="xl"
|
||||||
|
leftSection={<IconFileExcel size={16} />}
|
||||||
|
>
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -866,13 +892,22 @@ const Timekeeping = () => {
|
||||||
// offset={{ mainAxis: 5, crossAxis: 0 }}
|
// offset={{ mainAxis: 5, crossAxis: 0 }}
|
||||||
label={showTooltipAllNote(user)}
|
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 +
|
? import.meta.env.VITE_BACKEND_URL +
|
||||||
'storage/' +
|
'storage/' +
|
||||||
user.user.avatar
|
user.user.avatar
|
||||||
: import.meta.env.VITE_BACKEND_URL +
|
: import.meta.env.VITE_BACKEND_URL +
|
||||||
'image/storage/' +
|
'image/storage/' +
|
||||||
user.user.avatar}/>{user.user.name}</div>
|
user.user.avatar
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{user.user.name}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta={'center'}>{totalDays}</Table.Td>
|
<Table.Td ta={'center'}>{totalDays}</Table.Td>
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ const UsersManagement = () => {
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label={'Permission(s)'}
|
label={'Permission(s)'}
|
||||||
required
|
required
|
||||||
data={['staff', 'admin', 'hr', 'tester']}
|
data={['staff', 'admin', 'hr', 'tester', 'accountant']}
|
||||||
value={
|
value={
|
||||||
typeof form.values.permission === 'string'
|
typeof form.values.permission === 'string'
|
||||||
? form.values.permission
|
? form.values.permission
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import ResetPassword from '@/components/Authentication/ResetPassword'
|
import ResetPassword from '@/components/Authentication/ResetPassword'
|
||||||
import BasePage from '@/components/BasePage/BasePage'
|
import BasePage from '@/components/BasePage/BasePage'
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
||||||
|
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
|
||||||
import Allocation from '@/pages/Allocation/Allocation'
|
import Allocation from '@/pages/Allocation/Allocation'
|
||||||
import PageLogin from '@/pages/Auth/Login/Login'
|
import PageLogin from '@/pages/Auth/Login/Login'
|
||||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||||
|
|
@ -83,7 +84,7 @@ const mainRoutes = [
|
||||||
{
|
{
|
||||||
path: '/timekeeping',
|
path: '/timekeeping',
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute mode="home" permission="staff">
|
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||||
<BasePage
|
<BasePage
|
||||||
main={
|
main={
|
||||||
<>
|
<>
|
||||||
|
|
@ -97,7 +98,7 @@ const mainRoutes = [
|
||||||
{
|
{
|
||||||
path: '/leave-management',
|
path: '/leave-management',
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute mode="home" permission="staff">
|
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||||
<BasePage
|
<BasePage
|
||||||
main={
|
main={
|
||||||
<>
|
<>
|
||||||
|
|
@ -234,6 +235,20 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/profiles',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute mode="route" permission="admin,accountant">
|
||||||
|
<BasePage
|
||||||
|
main={
|
||||||
|
<>
|
||||||
|
<AllProfiles />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></BasePage>
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/packages',
|
// path: '/packages',
|
||||||
// element: (
|
// element: (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { getFormDataHeader, getHeaderInfo } from '@/rtk/helpers/tokenCreator'
|
||||||
import { removeTokens } from '@/rtk/localStorage'
|
import { removeTokens } from '@/rtk/localStorage'
|
||||||
import { notifications } from '@mantine/notifications'
|
import { notifications } from '@mantine/notifications'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import moment from 'moment'
|
||||||
const handleResponse = (response: any) => {
|
const handleResponse = (response: any) => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
removeTokens()
|
removeTokens()
|
||||||
|
|
@ -156,3 +157,45 @@ export const postImage = async (url: string, body: any, method: any) => {
|
||||||
throw handleResponse(err.response)
|
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