commit
8f6977be72
|
|
@ -14,7 +14,7 @@ class CategoryController extends Controller
|
|||
* @param Request $request The HTTP request object.
|
||||
* @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
|
||||
*/
|
||||
public function getListMaster(Request $request)
|
||||
public static function getListMaster(Request $request)
|
||||
{
|
||||
$data = Category::where('c_type', '=', $request->type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
|
||||
return AbstractController::ResultSuccess($data);
|
||||
|
|
@ -24,4 +24,9 @@ class CategoryController extends Controller
|
|||
$data = Category::where('c_type', '=', $type)->where('c_code', '=', $code)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->first();
|
||||
return $data;
|
||||
}
|
||||
public static function getListMasterByType($type)
|
||||
{
|
||||
$data = Category::where('c_type', '=', $type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class LeaveManagementController extends Controller
|
|||
})
|
||||
->leftJoin("categories as reason", function ($join) {
|
||||
$join->on('n_reason', '=', 'reason.c_code');
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
|
||||
})
|
||||
->select(
|
||||
DB::raw('notes.n_user_id as n_user_id'),
|
||||
|
|
@ -44,13 +44,14 @@ class LeaveManagementController extends Controller
|
|||
DB::raw('notes.n_month as month'),
|
||||
DB::raw('categories.c_value as leave_days'),
|
||||
DB::raw('notes.n_day as day'),
|
||||
DB::raw('notes.n_reason as reason_code'),
|
||||
'reason.c_name as reason_name',
|
||||
'categories.c_name as time_type_name',
|
||||
// DB::raw('SUM(categories.c_value) as leave_days')
|
||||
)
|
||||
// ->where('notes.n_user_id', "1")
|
||||
->where('notes.n_year', $year)
|
||||
->where('notes.n_reason', 'ONLEAVE')
|
||||
->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY', 'TEMPORARY_ONLEAVE'])
|
||||
// ->groupBy("notes.n_user_id")
|
||||
->orderBy('notes.n_month')
|
||||
->orderBy('notes.n_day')
|
||||
|
|
@ -59,7 +60,7 @@ class LeaveManagementController extends Controller
|
|||
return [
|
||||
"day" => $item->day,
|
||||
"n_user_id" => $item->n_user_id,
|
||||
// "time_type" => $item->time_type,
|
||||
"reason_code" => $item->reason_code,
|
||||
"reason_name" => $item->reason_name,
|
||||
"time_type_name" => $item->time_type_name,
|
||||
"month" => $item->month,
|
||||
|
|
@ -105,9 +106,10 @@ class LeaveManagementController extends Controller
|
|||
'leaveDay' => [
|
||||
'id' => $item->id,
|
||||
'ld_user_id' => $item->ld_user_id,
|
||||
'ld_day' => $item->ld_day,
|
||||
'ld_day_total' => $item->ld_day_total,
|
||||
'ld_year' => $item->ld_year,
|
||||
'ld_date_additional' => $item->ld_date_additional,
|
||||
'ld_additional_day' => $item->ld_additional_day,
|
||||
'ld_special_leave_day' => $item->ld_special_leave_day,
|
||||
'ld_note' => $item->ld_note,
|
||||
'created_at' => $item->created_at,
|
||||
'updated_at' => $item->updated_at,
|
||||
|
|
@ -133,8 +135,9 @@ class LeaveManagementController extends Controller
|
|||
$validatedData = $request->all();
|
||||
$leaveDays = LeaveDays::find($validatedData['id']);
|
||||
|
||||
$leaveDays->ld_day = $validatedData['totalLeave'];
|
||||
$leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
|
||||
$leaveDays->ld_day_total = $validatedData['totalLeave'];
|
||||
$leaveDays->ld_additional_day = $validatedData['dayAdditional'];
|
||||
$leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
|
||||
$leaveDays->ld_note = $validatedData['note'];
|
||||
|
||||
$leaveDays->save();
|
||||
|
|
@ -152,7 +155,7 @@ class LeaveManagementController extends Controller
|
|||
}
|
||||
|
||||
// Lọc chỉ lấy user có permission bao gồm staff
|
||||
$staffData = $leaveDays->filter(function($user) {
|
||||
$staffData = $leaveDays->filter(function ($user) {
|
||||
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
|
||||
});
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,12 +11,14 @@ use App\Traits\HasOrderByRequest;
|
|||
use App\Traits\HasSearchRequest;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\TicketMail;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\MonthlyTimekeeping;
|
||||
use Modules\Admin\app\Models\Tracking;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\TimekeepingExport;
|
||||
use Modules\Admin\app\Models\Ticket;
|
||||
|
||||
class TimekeepingController extends Controller
|
||||
{
|
||||
|
|
@ -152,8 +154,6 @@ class TimekeepingController extends Controller
|
|||
return response()->json(['status' => true, 'message' => 'Add successfully']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function updateCacheMonth(Request $request)
|
||||
{
|
||||
$month = $request->month;
|
||||
|
|
@ -172,20 +172,54 @@ class TimekeepingController extends Controller
|
|||
|
||||
// Validate the request
|
||||
$request->validate($rules);
|
||||
|
||||
$id = $request->input('id');
|
||||
|
||||
$month = $request->month;
|
||||
$year = $request->year;
|
||||
|
||||
$note = Notes::find($id);
|
||||
if ($note) {
|
||||
$note->delete();
|
||||
$this->createOrUpdateRecordForCurrentMonth($month, $year);
|
||||
return response()->json(['message' => 'Delete success', 'status' => true]);
|
||||
if (!$note) {
|
||||
return response()->json(['message' => 'Note not found', 'status' => false]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Delete fail', 'status' => false]);
|
||||
$ticket = Ticket::find($note->ticket_id);
|
||||
if (!$ticket) {
|
||||
return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
|
||||
}
|
||||
|
||||
$admin = auth('admins')->user();
|
||||
|
||||
// Handle send mail
|
||||
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->start_period);
|
||||
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->end_period);
|
||||
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $ticket->type);
|
||||
$formattedStartDate = Carbon::createFromFormat('Y-m-d', $ticket->start_date)->format('d/m/Y');
|
||||
$formattedEndDate = Carbon::createFromFormat('Y-m-d', $ticket->end_date)->format('d/m/Y');
|
||||
|
||||
$user = Admin::find($ticket->user_id);
|
||||
|
||||
$data = array(
|
||||
"email_template" => "email.notification_tickets_user",
|
||||
"user_name" => $user->name,
|
||||
"email" => $user->email,
|
||||
"name" => $admin->name, //name admin duyệt
|
||||
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
|
||||
"type" => $dataMasterType->c_name,
|
||||
"note" => $ticket->reason,
|
||||
"admin_note" => $ticket->admin_note,
|
||||
"link" => "/tickets", //link đến page admin
|
||||
"status" => "refused",
|
||||
"subject" => "[Ticket response] Ticket From " . $admin->name
|
||||
);
|
||||
Mail::to($user->email)->send(new TicketMail($data));
|
||||
|
||||
// Update
|
||||
$ticket->updated_by = $admin->name;
|
||||
$ticket->status = "REFUSED";
|
||||
$ticket->save();
|
||||
Notes::where('ticket_id', $ticket->id)->delete();
|
||||
|
||||
// Clear Timekeeping cache
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
|
||||
return response()->json(['message' => 'Delete success', 'status' => true]);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
|
|
@ -206,10 +240,10 @@ class TimekeepingController extends Controller
|
|||
}
|
||||
|
||||
// Lọc chỉ lấy user có permission bao gồm staff
|
||||
$staffData = array_filter($responseData['data'], function($user) {
|
||||
$staffData = array_filter($responseData['data'], function ($user) {
|
||||
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
|
||||
});
|
||||
|
||||
|
||||
$currentDate = date('d_His');
|
||||
return Excel::download(
|
||||
new TimekeepingExport(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ Route::middleware('api')
|
|||
Route::post('login', [AdminController::class, 'login']);
|
||||
Route::post('reset-password', [AdminController::class, 'resetPassword']);
|
||||
Route::get('forgot-password', [AdminController::class, 'forgotPassword']);
|
||||
Route::get('/email-handle-ticket', [TicketController::class, 'handleTicketEmail'])->name('email.ticket.handle');
|
||||
});
|
||||
|
||||
// NOTE after login
|
||||
|
|
@ -163,6 +164,7 @@ Route::middleware('api')
|
|||
], function () {
|
||||
Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
namespace Modules\Auth\app\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LeaveDays;
|
||||
use App\Traits\IsAPI;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Auth\app\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
use SimpleSoftwareIO\QrCode\Facades\QrCode;
|
||||
|
||||
class UserController extends Controller
|
||||
|
|
@ -34,9 +36,37 @@ class UserController extends Controller
|
|||
]);
|
||||
|
||||
if ($request->has('id')) {
|
||||
$payload = $request->only(['name', 'email', 'permission']);
|
||||
$payload = $request->only(['name', 'email', 'permission', 'is_permanent']);
|
||||
$user = User::find($request->id);
|
||||
|
||||
// Không cho chuyển từ chính thức thành lại thử việc
|
||||
if (!$request->is_permanent && $user->is_permanent) {
|
||||
return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']);
|
||||
}
|
||||
|
||||
// Thêm ngày phép khi thành nhân viên chính thức
|
||||
if ($request->is_permanent && !$user->is_permanent) {
|
||||
$userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', Carbon::now()->year)
|
||||
->first();
|
||||
|
||||
if ($userLeaveDay) {
|
||||
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
|
||||
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
|
||||
$userLeaveDay->ld_day_total = $permanentDefault;
|
||||
|
||||
$newNote = "Cộng ngày phép cho nhân viên chính thức"; // Thêm ghi chú
|
||||
if (!empty($userLeaveDay->ld_note)) {
|
||||
$userLeaveDay->ld_note = $userLeaveDay->ld_note . "\n" . $newNote;
|
||||
} else {
|
||||
$userLeaveDay->ld_note = $newNote;
|
||||
}
|
||||
$userLeaveDay->save();
|
||||
}
|
||||
|
||||
$payload['permanent_date'] = Carbon::now()->toDateString();
|
||||
}
|
||||
|
||||
$user->update($payload);
|
||||
return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
|
||||
} else {
|
||||
|
|
@ -44,9 +74,22 @@ class UserController extends Controller
|
|||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => bcrypt('Work@1234'),
|
||||
'permission' => $request->permission
|
||||
'permission' => $request->permission,
|
||||
'is_permanent' => false
|
||||
]);
|
||||
|
||||
// Khởi tạo LeaveDays cho nhân viên mới
|
||||
LeaveDays::insert([
|
||||
'ld_user_id' => $user->id,
|
||||
'ld_day_total' => 0,
|
||||
'ld_year' => Carbon::now()->year,
|
||||
'ld_additional_day' => 0,
|
||||
'ld_note' => '',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$this->createOrUpdateRecordForCurrentMonth(Carbon::now()->month, Carbon::now()->year);
|
||||
|
||||
$user_res = [
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
|
|
@ -98,8 +141,6 @@ class UserController extends Controller
|
|||
return response()->json(['data' => ['user' => $user_res, 'gitea' => "dev", 'zulip' => "dev"], 'status' => true, 'message' => 'Create successful']);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['status' => false, 'message' => 'Process fail']);
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ class User extends Authenticatable implements JWTSubject
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'permission'
|
||||
'permission',
|
||||
'is_permanent',
|
||||
'permanent_date'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\AddMonthlyLeaveDays;
|
||||
|
||||
class AddMonthlyLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'add:monthly-leavedays {month?} {year?}';
|
||||
protected $description = 'Cộng 1 ngày phép hàng tháng cho tất cả người dùng';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$month = $this->argument('month');
|
||||
$year = $this->argument('year');
|
||||
AddMonthlyLeaveDays::dispatch($month, $year);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use App\Jobs\InitializeLeaveDays;
|
|||
class InitializeLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'initialize:leavedays {year?}';
|
||||
protected $description = 'Initialize leave days for users';
|
||||
protected $description = 'Cấp phép năm cho tất cả người dùng';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -18,6 +18,7 @@ class InitializeLeaveDaysCommand extends Command
|
|||
public function handle()
|
||||
{
|
||||
$year = $this->argument('year');
|
||||
InitializeLeaveDays::dispatch($year);
|
||||
// Không sử dụng nữa, theo rule mới
|
||||
// InitializeLeaveDays::dispatch($year);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\UpdateTemporaryLeaveDays;
|
||||
|
||||
class UpdateTemporaryLeaveDaysCommand extends Command
|
||||
{
|
||||
protected $signature = 'update:temporary-leavedays {month?} {year?}';
|
||||
protected $description = 'Tính lại ngày phép cho các note tạm.';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$month = $this->argument('month');
|
||||
$year = $this->argument('year');
|
||||
UpdateTemporaryLeaveDays::dispatch($month, $year);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Console;
|
||||
|
||||
use App\Jobs\DeductLeaveDays;
|
||||
use App\Jobs\AddMonthlyLeaveDays;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
|
|
@ -32,6 +33,10 @@ class Kernel extends ConsoleKernel
|
|||
|
||||
// Chạy buổi chiều lúc 17:30
|
||||
$schedule->command('attendance:check C')->dailyAt('17:30');
|
||||
|
||||
// Chạy vào ngày đầu tiên của mỗi tháng
|
||||
$schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
|
||||
$schedule->command('update:temporary-leavedays')->monthlyOn(1, '00:05');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, With
|
|||
$stt = 0;
|
||||
foreach ($this->data as $index => $user) {
|
||||
$totalDayOff = 0;
|
||||
$totalDayLeave = $user['leaveDay']['ld_day'] + $user['leaveDay']['ld_date_additional'];
|
||||
$totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day'];
|
||||
|
||||
// Tính tổng ngày nghỉ theo tháng
|
||||
$monthlyLeaves = array_fill(1, 12, 0);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\LeaveDays;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
|
||||
class AddMonthlyLeaveDays implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $month;
|
||||
protected $year;
|
||||
|
||||
private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng
|
||||
|
||||
public function __construct($month = null, $year = null)
|
||||
{
|
||||
$this->month = $month ?? Carbon::now()->month;
|
||||
$this->year = $year ?? Carbon::now()->year;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$users = User::get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Nếu là nhân viên chưa chính thức, ko cộng phép
|
||||
if (!$user->is_permanent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
|
||||
if (!$leaveDay) {
|
||||
// Nếu chưa có dữ liệu năm hiện tại, tạo mới
|
||||
// Số ngày phép bằng với tháng hiện tại
|
||||
$leaveDay = new LeaveDays([
|
||||
'ld_user_id' => $user->id,
|
||||
'ld_day_total' => $this->month, // Số ngày phép bằng tháng hiện tại
|
||||
'ld_year' => $this->year,
|
||||
'ld_additional_day' => 0,
|
||||
'ld_note' => 'Khởi tạo ngày phép đến tháng ' . $this->month,
|
||||
'ld_special_leave_day' => 0,
|
||||
]);
|
||||
$leaveDay->save();
|
||||
} else {
|
||||
// Check có phải là nhân viên chính thức trong năm nay (Nhân viên mới)
|
||||
if ($user->permanent_date && $user->permanent_date !== '0000-00-00') {
|
||||
$permenantYear = Carbon::parse($user->permanent_date)->year;
|
||||
|
||||
if ($permenantYear === $this->year) {
|
||||
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
|
||||
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
|
||||
|
||||
$permanentMonth = Carbon::parse($user->permanent_date)->month;
|
||||
if ($this->month > $leaveDay->ld_day_total - ($permanentDefault - $permanentMonth)) {
|
||||
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
|
||||
$leaveDay->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại (Nhân viên cũ)
|
||||
if ($leaveDay->ld_day_total < $this->month) {
|
||||
// Cộng mỗi tháng 1 ngày phép cho nhân viên
|
||||
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
|
||||
$leaveDay->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DeductLeaveDays implements ShouldQueue
|
||||
|
|
@ -36,44 +35,34 @@ class DeductLeaveDays implements ShouldQueue
|
|||
foreach ($users as $user) {
|
||||
$existingData = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->where('ld_date_additional', ">", 0)
|
||||
->where('ld_additional_day', ">", 0)
|
||||
->first();
|
||||
if (!$existingData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
|
||||
// Lấy tổng ngày nghỉ phép 3 tháng đầu trong năm
|
||||
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
|
||||
$join->on('notes.n_time_type', '=', 'categories.c_code')
|
||||
->where('categories.c_type', 'TIME_TYPE');
|
||||
})
|
||||
->select(
|
||||
DB::raw('notes.n_user_id as n_user_id'),
|
||||
DB::raw('notes.n_year as year'),
|
||||
DB::raw('SUM(categories.c_value) as leave_days')
|
||||
)
|
||||
->where('notes.n_year', $this->year)
|
||||
->where('notes.n_user_id', $user->id)
|
||||
->where('notes.n_reason', 'ONLEAVE')
|
||||
->groupBy(DB::raw('notes.n_year'))
|
||||
->first();
|
||||
|
||||
if ($totalLeaveDaysByMonth) {
|
||||
//Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng)
|
||||
if ($existingData->ld_date_additional > $totalLeaveDaysByMonth->leave_days) {
|
||||
LeaveDays::where('ld_year', $this->year)
|
||||
->where('ld_user_id', $user->id)
|
||||
->update([
|
||||
'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
|
||||
]);
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', "<=", 3)
|
||||
->where('n_reason', 'ONLEAVE')
|
||||
->sum('categories.c_value');
|
||||
|
||||
if ($usedOnleaveDaysTotal) {
|
||||
if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
|
||||
$ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
|
||||
$existingData->ld_note = $existingData->ld_note . "\n" . $ld_note;
|
||||
$existingData->ld_additional_day = $usedOnleaveDaysTotal;
|
||||
}
|
||||
} else {
|
||||
//Nếu không sử dụng ngày nghỉ còn lại ở năm rồi thì xóa => theo luật ld
|
||||
LeaveDays::where('ld_year', $this->year)
|
||||
->where('ld_user_id', $user->id)
|
||||
->update([
|
||||
'ld_date_additional' => "0",
|
||||
]);
|
||||
$existingData->ld_additional_day = 0;
|
||||
}
|
||||
|
||||
$existingData->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
public function handle(): void
|
||||
{
|
||||
$users = User::get();
|
||||
$ld_day = 12;
|
||||
$ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
|
||||
$existingData = LeaveDays::where('ld_user_id', $user->id)
|
||||
|
|
@ -51,11 +52,11 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
->where('ld_year', $this->year - 1)
|
||||
->first();
|
||||
|
||||
$ld_date_additional = 0;
|
||||
$ld_additional_day = 0;
|
||||
$ld_note = '';
|
||||
|
||||
if ($previousYearData) {
|
||||
$ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional;
|
||||
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day;
|
||||
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
|
||||
$join->on('notes.n_time_type', '=', 'categories.c_code')
|
||||
->where('categories.c_type', 'TIME_TYPE');
|
||||
|
|
@ -71,20 +72,23 @@ class InitializeLeaveDays implements ShouldQueue
|
|||
->groupBy(DB::raw('notes.n_year'))
|
||||
->first();
|
||||
if ($totalLeaveDaysByMonth) {
|
||||
$ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
|
||||
if ($ld_date_additional < 0) {
|
||||
$ld_date_additional = 0;
|
||||
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
|
||||
if ($ld_additional_day < 0) {
|
||||
$ld_additional_day = 0;
|
||||
}
|
||||
}
|
||||
$ld_note = 'Cộng dồn ngày phép năm cũ';
|
||||
|
||||
if ($ld_additional_day > 0) {
|
||||
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
|
||||
}
|
||||
}
|
||||
|
||||
// Tạo dữ liệu cho năm hiện tại
|
||||
LeaveDays::insert([
|
||||
'ld_user_id' => $user->id,
|
||||
'ld_day' => $ld_day,
|
||||
'ld_day_total' => $user->is_permanent ? $ld_day_total : 0, // Nếu là nhân viên mới, ko cấp phép
|
||||
'ld_year' => $this->year,
|
||||
'ld_date_additional' => $ld_date_additional,
|
||||
'ld_additional_day' => $ld_additional_day,
|
||||
'ld_note' => $ld_note,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\LeaveDays;
|
||||
use App\Models\Notes;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Admin\app\Models\Category;
|
||||
|
||||
class UpdateTemporaryLeaveDays implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
protected $month;
|
||||
protected $year;
|
||||
|
||||
public function __construct($month = null, $year = null)
|
||||
{
|
||||
$this->month = $month ?? Carbon::now()->month;
|
||||
$this->year = $year ?? Carbon::now()->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$users = User::get();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
|
||||
->where('ld_year', $this->year)
|
||||
->first();
|
||||
|
||||
$notes = Notes::where('n_reason', 'TEMPORARY_ONLEAVE')
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', $this->month)
|
||||
->whereExists(function ($query) use ($user) {
|
||||
$query->select(DB::raw(1))
|
||||
->from('tickets')
|
||||
->where('tickets.user_id', $user->id)
|
||||
->where('tickets.status', 'CONFIRMED')
|
||||
->where('tickets.type', 'ONLEAVE');
|
||||
})
|
||||
->get();
|
||||
|
||||
$maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
|
||||
|
||||
// Tổng ngày nghỉ sẽ dùng trong tháng
|
||||
$willUsedDaysInMonth = 0;
|
||||
foreach ($notes as $note) {
|
||||
$willUsedDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
|
||||
}
|
||||
|
||||
// Tổng phép đang có
|
||||
$onleaveDaysTotal = $leaveDay->ld_day_total + $leaveDay->ld_additional_day + $leaveDay->ld_special_leave_day;
|
||||
// Phép đã sử dụng tới tháng hiện tại
|
||||
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
|
||||
$join->on('notes.n_time_type', '=', 'categories.c_code')
|
||||
->where('categories.c_type', 'TIME_TYPE');
|
||||
})
|
||||
->where('n_user_id', $user->id)
|
||||
->where('n_year', $this->year)
|
||||
->where('n_month', "<=", $this->month)
|
||||
->where('n_reason', 'ONLEAVE')
|
||||
->sum('categories.c_value');
|
||||
// Phép còn lại
|
||||
$remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal;
|
||||
|
||||
// Log::debug("User {$user->name}\n");
|
||||
// Log::debug(
|
||||
// "📊 Thống kê ngày phép:\n" .
|
||||
// " - Tháng: {$this->month}\n" .
|
||||
// " - Tổng ngày nghỉ sẽ dùng trong tháng: $willUsedDaysInMonth\n" .
|
||||
// " - Tổng ngày phép: $onleaveDaysTotal\n" .
|
||||
// " - Tổng ngày phép đã nghỉ: $usedOnleaveDaysTotal\n" .
|
||||
// " - Tổng ngày phép còn lại: $remainingOnleaveDays\n"
|
||||
// );
|
||||
|
||||
$onleave_days_will_use = 0; // Ngày phép sẽ dùng
|
||||
$nopay_days_will_use = 0; // Ngày ko phép sẽ dùng
|
||||
|
||||
// Ngày phép còn lại <= 0 (Hết phép)
|
||||
if ($remainingOnleaveDays <= 0) {
|
||||
$onleave_days_will_use = 0;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth;
|
||||
|
||||
Log::debug("--- Hết phép trong tháng ---");
|
||||
}
|
||||
|
||||
// Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
|
||||
else if ($remainingOnleaveDays < $willUsedDaysInMonth) {
|
||||
// Vượt limit
|
||||
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
|
||||
// Phép còn lại > limit
|
||||
if ($remainingOnleaveDays > $maxDaysPerMonth) {
|
||||
$onleave_days_will_use = $maxDaysPerMonth;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
|
||||
}
|
||||
// Phép còn lại < limit
|
||||
else {
|
||||
$onleave_days_will_use = $remainingOnleaveDays;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
|
||||
}
|
||||
|
||||
Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---",);
|
||||
}
|
||||
// Không vượt limit
|
||||
else {
|
||||
$onleave_days_will_use = $remainingOnleaveDays;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
|
||||
|
||||
Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---");
|
||||
}
|
||||
}
|
||||
|
||||
// Ngày phép còn lại >= ngày yêu cầu (Đủ phép)
|
||||
else {
|
||||
// Vượt limit
|
||||
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
|
||||
$onleave_days_will_use = $maxDaysPerMonth;
|
||||
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
|
||||
|
||||
Log::debug("--- Đủ phép, vượt limit ---");
|
||||
}
|
||||
// Không vượt limit
|
||||
else {
|
||||
$onleave_days_will_use = $willUsedDaysInMonth;
|
||||
$nopay_days_will_use = 0;
|
||||
|
||||
Log::debug("--- Đủ phép ---");
|
||||
}
|
||||
}
|
||||
|
||||
Log::debug("", [
|
||||
"Phep" => $onleave_days_will_use,
|
||||
"Khong Phep" => $nopay_days_will_use
|
||||
]);
|
||||
|
||||
// Có nghỉ không phép
|
||||
if ($nopay_days_will_use > 0) {
|
||||
foreach ($notes as $note) {
|
||||
$value = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
|
||||
|
||||
if ($note->n_time_type === 'ALL' && $onleave_days_will_use == 0.5) {
|
||||
// Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép
|
||||
// Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C)
|
||||
|
||||
Notes::create([
|
||||
'n_user_id' => $user->id,
|
||||
'n_day' => $note->n_day,
|
||||
'n_month' => $note->n_month,
|
||||
'n_year' => $note->n_year,
|
||||
'n_time_type' => 'S',
|
||||
'n_reason' => 'ONLEAVE',
|
||||
'n_note' => $note->n_note,
|
||||
'ticket_id' => $note->ticket_id
|
||||
]);
|
||||
Notes::create([
|
||||
'n_user_id' => $user->id,
|
||||
'n_day' => $note->n_day,
|
||||
'n_month' => $note->n_month,
|
||||
'n_year' => $note->n_year,
|
||||
'n_time_type' => 'C',
|
||||
'n_reason' => 'LEAVE_WITHOUT_PAY',
|
||||
'n_note' => $note->n_note,
|
||||
'ticket_id' => $note->ticket_id
|
||||
]);
|
||||
|
||||
$note->delete();
|
||||
|
||||
$onleave_days_will_use = 0;
|
||||
$nopay_days_will_use -= 0.5;
|
||||
} elseif ($onleave_days_will_use > 0) {
|
||||
// Dùng ngày phép trước
|
||||
$use = min($onleave_days_will_use, $value);
|
||||
$note->update([
|
||||
'n_reason' => "ONLEAVE"
|
||||
]);
|
||||
$onleave_days_will_use -= $use;
|
||||
} elseif ($nopay_days_will_use > 0) {
|
||||
// Hết phép, chuyển sang không phép
|
||||
$use = min($nopay_days_will_use, $value);
|
||||
$note->update([
|
||||
'n_reason' => "LEAVE_WITHOUT_PAY"
|
||||
]);
|
||||
$nopay_days_will_use -= $use;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Đủ phép
|
||||
else {
|
||||
foreach ($notes as $note) {
|
||||
$note->update([
|
||||
'n_reason' => "ONLEAVE"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getMaxLeaveDaysPerMonth(): int
|
||||
{
|
||||
$limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first();
|
||||
if ($limitLeaveMonth) {
|
||||
$maxDaysPerMonth = (int)$limitLeaveMonth->c_value;
|
||||
} else {
|
||||
$maxDaysPerMonth = 3; // default nếu k có setting
|
||||
}
|
||||
return $maxDaysPerMonth;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ class LeaveDays extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note'
|
||||
'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note'
|
||||
];
|
||||
|
||||
protected $table = 'leave_days';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,14 @@ class Notes extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
|
||||
'n_user_id',
|
||||
'n_day',
|
||||
'n_month',
|
||||
'n_year',
|
||||
'n_time_type',
|
||||
'n_reason',
|
||||
'n_note',
|
||||
'ticket_id'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -25,7 +32,7 @@ class Notes extends Model
|
|||
{
|
||||
return self::leftJoin("categories as reason", function ($join) {
|
||||
$join->on('n_reason', '=', 'reason.c_code');
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
|
||||
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
|
||||
})
|
||||
->leftJoin("categories as timeTypes", function ($join) {
|
||||
$join->on('n_time_type', '=', 'timeTypes.c_code');
|
||||
|
|
@ -47,4 +54,18 @@ class Notes extends Model
|
|||
)
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getNotesByMonthAndYearAndUserId($month, $year, $userId, $idNote)
|
||||
{
|
||||
return self::where('n_reason', 'ONLEAVE')->where('n_month', $month)->where('n_year', $year)
|
||||
->where('n_user_id', $userId)
|
||||
->where('id', '!=', $idNote)->get();
|
||||
}
|
||||
|
||||
public static function getNotesByMonthAndYearAndUserIdAndReason($month, $year, $userId, $reason)
|
||||
{
|
||||
return self::where('n_reason', $reason)->where('n_month', $month)->where('n_year', $year)
|
||||
->where('n_user_id', $userId)
|
||||
->orderBy('n_day', 'asc')->orderBy('n_time_type', 'desc')->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ return [
|
|||
|
||||
'asset_url' => env('ASSET_URL'),
|
||||
|
||||
'client_url' => env('ADMIN_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RenameLdDayToLdDayTotalInLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_day', 'ld_day_total');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_day_total', 'ld_day');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLdSpecialLeaveDayToLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->float('ld_special_leave_day')->default(0); // Adding the new field
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->dropColumn('ld_special_leave_day'); // Dropping the field if needed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class RenameLdDateAdditionalToLdAdditionalDayInLeaveDaysTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_date_additional', 'ld_additional_day');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('leave_days', function (Blueprint $table) {
|
||||
$table->renameColumn('ld_date_additional', 'ld_additional_day');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
[
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Nghỉ không hưởng lương',
|
||||
'c_type' => 'REASON',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép')
|
||||
->update(['c_name' => 'Nghỉ phép năm']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép năm')
|
||||
->update(['c_name' => 'Nghỉ phép']);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateLeaveCategories extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Xóa item với type REASON và code LEAVE_WITHOUT_PAY
|
||||
DB::table('categories')
|
||||
->where('c_type', 'REASON')
|
||||
->where('c_code', 'LEAVE_WITHOUT_PAY')
|
||||
->delete();
|
||||
|
||||
// Cập nhật tên "Nghỉ phép năm" thành "Nghỉ phép"
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép năm')
|
||||
->update(['c_name' => 'Nghỉ phép']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Khôi phục item đã xóa
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Không phép',
|
||||
'c_type' => 'REASON',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Khôi phục tên cũ
|
||||
DB::table('categories')
|
||||
->where('c_name', 'Nghỉ phép')
|
||||
->update(['c_name' => 'Nghỉ phép năm']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddLimitLeaveMonthCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => 'LIMIT',
|
||||
'c_name' => 'Giới hạn số ngày nghỉ có phép/tháng',
|
||||
'c_type' => 'LIMIT_LEAVE_MONTH',
|
||||
'c_value' => '3',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', 'LIMIT')
|
||||
->where('c_type', 'LIMIT_LEAVE_MONTH')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddSaturdayWorkScheduleCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => '10-05-2025',
|
||||
'c_name' => 'Ngày bắt đầu làm việc thứ 7 trong năm',
|
||||
'c_type' => 'SATURDAY_WORK_SCHEDULE',
|
||||
'c_value' => '2025',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', '10-05-2025')
|
||||
->where('c_type', 'SATURDAY_WORK_SCHEDULE')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddDayWorkSpecialCategory extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
'c_code' => '17-05-2025',
|
||||
'c_name' => 'Ngày làm việc đặc biệt',
|
||||
'c_type' => 'DAY_WORK_SPECIAL',
|
||||
'c_value' => '2025',
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->where('c_code', '17-05-2025')
|
||||
->where('c_type', 'DAY_WORK_SPECIAL')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddLeaveCategories extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
[
|
||||
'c_code' => 'LEAVE_WITHOUT_PAY',
|
||||
'c_name' => 'Không phép',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'WFH',
|
||||
'c_name' => 'Work From Home',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'ONLEAVE',
|
||||
'c_name' => 'Nghỉ phép',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('categories')
|
||||
->whereIn('c_code', ['LEAVE_WITHOUT_PAY', 'WFH', 'ONLEAVE'])
|
||||
->where('c_type', 'REASON_NOTES')
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('notes', function (Blueprint $table) {
|
||||
$table->foreignId('ticket_id')
|
||||
->nullable()
|
||||
->constrained('tickets')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('notes', function (Blueprint $table) {
|
||||
$table->dropForeign(['ticket_id']);
|
||||
$table->dropColumn('ticket_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_permanent')->default(true);
|
||||
$table->date('permanent_date');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_permanent');
|
||||
$table->dropColumn('permanent_date');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('categories')->insert([
|
||||
[
|
||||
'c_code' => 'PERMANENT',
|
||||
'c_name' => 'Phép cộng nhân viên chính thức',
|
||||
'c_type' => 'PERMANENT_ONLEAVE',
|
||||
'c_value' => 1,
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'c_code' => 'TEMPORARY_ONLEAVE',
|
||||
'c_name' => 'Nghỉ dự kiến',
|
||||
'c_type' => 'REASON_NOTES',
|
||||
'c_value' => "",
|
||||
'c_active' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('categories')->where('c_code', 'PERMANENT')->where('c_type', 'PERMANENT_ONLEAVE')->delete();
|
||||
DB::table('categories')->where('c_code', 'TEMPORARY_ONLEAVE')->where('c_type', 'REASON_NOTES')->delete();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
|
@ -37,119 +36,167 @@
|
|||
<title>{{ $data['subject'] }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<body style="
|
||||
<body style="
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: #edf2f7;
|
||||
">
|
||||
<table style="margin: 0 auto">
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
style="
|
||||
<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="
|
||||
<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">
|
||||
Dear Admin,
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
|
||||
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a request ticket, the specific content is as follows:
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
|
||||
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
|
||||
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
|
||||
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
|
||||
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
|
||||
<a href='{{ config('app.url') . $data['link'] }}'
|
||||
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="
|
||||
<tr>
|
||||
<td>
|
||||
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
|
||||
Dear Admin,
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
|
||||
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a <a href="{{ config('app.client_url') . $data['link'] }}"> request ticket</a>, the specific content is as follows:
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
|
||||
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
|
||||
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
|
||||
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
|
||||
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<p style="text-align: center">
|
||||
You can quick
|
||||
<span style="font-weight: bold">Confirm</span> or
|
||||
<span style="font-weight: bold">Refuse</span> here:
|
||||
</p>
|
||||
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
">
|
||||
<a
|
||||
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'confirm', 'admin_email' => $data['admin_email']]) }}"
|
||||
style="
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
background-color: #12b886;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.2) 30%,
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
">
|
||||
Confirm</a>
|
||||
|
||||
<a
|
||||
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'refuse', 'admin_email' => $data['admin_email']]) }}"
|
||||
style="
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
background-color: #f03e3e;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgba(0, 0, 0, 0.2),
|
||||
rgba(0, 0, 0, 0.2) 30%,
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
">
|
||||
Refuse</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="color: #222222;">
|
||||
<div style="margin-top: 3rem">
|
||||
<p><span style="font-weight: bold">Note</span>: If you are redirected to a <span style="font-weight: bold">404 page</span>, it means:</p>
|
||||
<p>1. The ticket has already been approved by another admin.</p>
|
||||
<p>2. The ticket has been deleted.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
style="
|
||||
margin: 0 auto;
|
||||
|
||||
width: 768px;
|
||||
">
|
||||
<tr>
|
||||
<td>
|
||||
<h5
|
||||
style="
|
||||
<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>
|
||||
<p>© 2024 APAC Tech.</p>
|
||||
</h5>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
require_once __DIR__ . "/../vendor/autoload.php";
|
||||
$app = include_once __DIR__ . '/../bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
// Cập nhật lại data cho tới tháng hiện tại
|
||||
$currentMonth = Carbon::now()->month;
|
||||
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
|
||||
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
|
|
@ -48,6 +48,7 @@ export const getTickets = API_URL + 'v1/admin/ticket/all'
|
|||
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
|
||||
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
|
||||
export const addTicket = API_URL + 'v1/admin/ticket/create'
|
||||
export const updateTicket = API_URL + 'v1/admin/ticket/update'
|
||||
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
|
||||
|
||||
//Users
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { getLeaveManagement, updateNoteLeave, exportLeaveManagement } from '@/api/Admin'
|
||||
import { update } from '@/rtk/helpers/CRUD'
|
||||
import { get, exportFile } from '@/rtk/helpers/apiService'
|
||||
import { useEffect, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Drawer,
|
||||
Flex,
|
||||
Group,
|
||||
HoverCard,
|
||||
Menu,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
|
|
@ -17,13 +21,17 @@ import {
|
|||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { IconEdit, IconFileExcel } from '@tabler/icons-react'
|
||||
|
||||
import { IconEdit, IconFileExcel, IconHelpCircle } from '@tabler/icons-react'
|
||||
import classes from './LeaveManagement.module.css'
|
||||
|
||||
import {
|
||||
getLeaveManagement,
|
||||
updateNoteLeave,
|
||||
exportLeaveManagement,
|
||||
} from '@/api/Admin'
|
||||
import { update } from '@/rtk/helpers/CRUD'
|
||||
import { get, exportFile } from '@/rtk/helpers/apiService'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
|
|
@ -40,8 +48,9 @@ interface LeaveDay {
|
|||
id: number
|
||||
ld_user_id: number
|
||||
ld_year: number
|
||||
ld_day: number
|
||||
ld_date_additional: number
|
||||
ld_day_total: number
|
||||
ld_additional_day: number
|
||||
ld_special_leave_day: number
|
||||
ld_note: string
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
|
|
@ -52,6 +61,7 @@ interface MonthlyLeaveDays {
|
|||
leave_days: number
|
||||
month: number
|
||||
n_user_id: number
|
||||
reason_code: string
|
||||
reason_name: string
|
||||
time_type_name: string
|
||||
}
|
||||
|
|
@ -62,6 +72,18 @@ interface UserData {
|
|||
monthlyLeaveDays: MonthlyLeaveDays[]
|
||||
}
|
||||
|
||||
// interface DataReason {
|
||||
// id: number
|
||||
// c_code: string
|
||||
// c_name: string
|
||||
// }
|
||||
|
||||
// interface DataTimeType {
|
||||
// id: number
|
||||
// c_code: string
|
||||
// c_name: string
|
||||
// }
|
||||
|
||||
const LeaveManagement = () => {
|
||||
const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
|
||||
const [disableBtn, setDisableBtn] = useState(false)
|
||||
|
|
@ -75,6 +97,7 @@ const LeaveManagement = () => {
|
|||
note: string
|
||||
totalLeave: string
|
||||
dayAdditional: string
|
||||
specialLeave: string
|
||||
}>({
|
||||
id: 0,
|
||||
user: {
|
||||
|
|
@ -84,12 +107,53 @@ const LeaveManagement = () => {
|
|||
note: '',
|
||||
totalLeave: '',
|
||||
dayAdditional: '',
|
||||
specialLeave: '',
|
||||
})
|
||||
|
||||
const [data, setData] = useState<UserData[]>([])
|
||||
const [date, setDate] = useState({
|
||||
year: new Date().getFullYear().toString(),
|
||||
})
|
||||
// const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
|
||||
// const [dataReason, setDataReason] = useState<DataReason[]>([])
|
||||
|
||||
const now = new Date()
|
||||
const currentMonth = now.getMonth() + 1 // getMonth() trả về 0-11
|
||||
const currentYear = now.getFullYear()
|
||||
|
||||
// const getListMasterByType = async (type: string) => {
|
||||
// try {
|
||||
// const params = {
|
||||
// type: type,
|
||||
// }
|
||||
// const res = await get(getListMaster, params)
|
||||
// if (res.status) {
|
||||
// return res.data
|
||||
// }
|
||||
// } catch (error: any) {
|
||||
// notifications.show({
|
||||
// title: 'Error',
|
||||
// message: error.message ?? error,
|
||||
// color: 'red',
|
||||
// })
|
||||
// }
|
||||
// return []
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// const fetchData = async () => {
|
||||
// const resultTimeType = await getListMasterByType('TIME_TYPE')
|
||||
// setDataTimeType(
|
||||
// resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
|
||||
// )
|
||||
|
||||
// const resultReason = await getListMasterByType('REASON')
|
||||
// setDataReason(resultReason)
|
||||
// }
|
||||
|
||||
// fetchData()
|
||||
// }, [])
|
||||
|
||||
const getLeaveList = async () => {
|
||||
try {
|
||||
const res = await get(getLeaveManagement, {
|
||||
|
|
@ -122,6 +186,7 @@ const LeaveManagement = () => {
|
|||
},
|
||||
totalLeave: string,
|
||||
dayAdditional: string,
|
||||
specialLeave: string,
|
||||
note: string,
|
||||
) => {
|
||||
try {
|
||||
|
|
@ -132,6 +197,7 @@ const LeaveManagement = () => {
|
|||
users: users,
|
||||
totalLeave: totalLeave,
|
||||
dayAdditional: dayAdditional,
|
||||
specialLeave: specialLeave,
|
||||
note: note,
|
||||
},
|
||||
getLeaveList,
|
||||
|
|
@ -202,8 +268,6 @@ const LeaveManagement = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// console.log(customAddNotes, 'customAddNotes')
|
||||
|
||||
const getDetailLeaveDay = (monthlyLeaveDays: MonthlyLeaveDays[]) => {
|
||||
type MonthlyLeaveDaysAcc = {
|
||||
[key: string]: { n_user_id: number; month: number; leave_days: number }
|
||||
|
|
@ -227,37 +291,97 @@ const LeaveManagement = () => {
|
|||
}
|
||||
|
||||
const showAllOff = (monthlyLeaveDays: MonthlyLeaveDays[]) => {
|
||||
let lastmonth = 0
|
||||
return monthlyLeaveDays.map((itemDay, indexDay) => {
|
||||
const isNewMonth = lastmonth !== itemDay.month
|
||||
if (isNewMonth) {
|
||||
lastmonth = itemDay.month
|
||||
return monthInYear.map((d, i) => {
|
||||
let totalOnLeaveMonth = 0
|
||||
let totalLeaveWithoutPayMonth = 0
|
||||
let totalTempMonth = 0
|
||||
|
||||
monthlyLeaveDays
|
||||
.filter((item) => item.month === d.value)
|
||||
.map((item) => {
|
||||
if (item.reason_code === 'ONLEAVE') {
|
||||
totalOnLeaveMonth += Number(item.leave_days)
|
||||
} else if (item.reason_code === 'LEAVE_WITHOUT_PAY') {
|
||||
totalLeaveWithoutPayMonth += Number(item.leave_days)
|
||||
} else {
|
||||
totalTempMonth += Number(item.leave_days)
|
||||
}
|
||||
})
|
||||
|
||||
if (totalOnLeaveMonth === 0 && totalLeaveWithoutPayMonth === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={indexDay}>
|
||||
{isNewMonth && <p>Month {lastmonth}</p>}
|
||||
<p style={{ paddingLeft: '20px' }}>
|
||||
- {itemDay.reason_name} ({itemDay.time_type_name}) {itemDay.day}
|
||||
/{itemDay.month}
|
||||
</p>
|
||||
</div>
|
||||
<Box key={i} px="xs" my="xs">
|
||||
<Group gap="xs">
|
||||
{totalOnLeaveMonth > 0 && (
|
||||
<Badge color="teal" variant="light">
|
||||
{totalOnLeaveMonth} có phép
|
||||
</Badge>
|
||||
)}
|
||||
{totalLeaveWithoutPayMonth > 0 && (
|
||||
<Badge color="red" variant="light">
|
||||
{totalLeaveWithoutPayMonth} không phép
|
||||
</Badge>
|
||||
)}
|
||||
<Text size="xs" color="dimmed">
|
||||
({d.value}/2025)
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const showAllTotal = (
|
||||
ld_day_total: number,
|
||||
ld_additional_day: number,
|
||||
ld_special_leave_day: number,
|
||||
): JSX.Element => {
|
||||
const showItem = (label: string, value: number, color: string = 'gray') => {
|
||||
if (value === 0) return null
|
||||
|
||||
return (
|
||||
<Group justify="space-between" gap="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="sm" fw={500} c={color}>
|
||||
{value}
|
||||
</Text>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p="sm">
|
||||
<Stack gap={4}>
|
||||
{showItem(
|
||||
'Tổng phép hiện có:',
|
||||
ld_day_total + ld_additional_day + ld_special_leave_day,
|
||||
'white',
|
||||
)}
|
||||
{showItem('+ Phép được cấp năm nay:', ld_day_total, 'teal')}
|
||||
{showItem('+ Phép tồn năm trước:', ld_additional_day, 'violet')}
|
||||
{showItem('+ Phép đặc biệt:', ld_special_leave_day, 'orange')}
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const timestamp = moment().format('DDMMYYYY_HHmmss')
|
||||
const fileName = `LeaveManagement_${date.year}_${timestamp}.xlsx`
|
||||
|
||||
|
||||
await exportFile(
|
||||
exportLeaveManagement,
|
||||
{
|
||||
year: parseInt(date.year)
|
||||
year: parseInt(date.year),
|
||||
},
|
||||
fileName
|
||||
fileName,
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
notifications.show({
|
||||
|
|
@ -271,10 +395,9 @@ const LeaveManagement = () => {
|
|||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
<h3>
|
||||
Leave Management
|
||||
</h3>
|
||||
<h3>Leave Management</h3>
|
||||
</div>
|
||||
{/* Update Leave Day */}
|
||||
<Drawer
|
||||
opened={opened1}
|
||||
onClose={close1}
|
||||
|
|
@ -305,8 +428,8 @@ const LeaveManagement = () => {
|
|||
})
|
||||
}
|
||||
}}
|
||||
label={'Total Leave'}
|
||||
placeholder="Input placeholder"
|
||||
label={'Phép năm'}
|
||||
placeholder="Nhập số ngày phép năm"
|
||||
/>
|
||||
<TextInput
|
||||
mb={'md'}
|
||||
|
|
@ -332,10 +455,36 @@ const LeaveManagement = () => {
|
|||
})
|
||||
}
|
||||
}}
|
||||
label={'Day additional leave'}
|
||||
placeholder="Input placeholder"
|
||||
label={'Phép năm cũ'}
|
||||
placeholder="Nhập số ngày phép năm cũ"
|
||||
/>
|
||||
<TextInput
|
||||
mb={'md'}
|
||||
value={customAddNotes.specialLeave}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value) {
|
||||
const floatValue = parseFloat(value)
|
||||
if (
|
||||
/^\d*\.?\d?$/.test(value) &&
|
||||
floatValue >= 0 &&
|
||||
floatValue <= 20
|
||||
) {
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
specialLeave: value,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
specialLeave: '',
|
||||
})
|
||||
}
|
||||
}}
|
||||
label={'Phép đặc biệt'}
|
||||
placeholder="Nhập số ngày phép đặc biệt"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
mb={'md'}
|
||||
label="Note"
|
||||
|
|
@ -343,6 +492,7 @@ const LeaveManagement = () => {
|
|||
onChange={(e) => {
|
||||
setCustomAddNotes({ ...customAddNotes, note: e.target.value })
|
||||
}}
|
||||
rows={10}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
|
@ -368,6 +518,7 @@ const LeaveManagement = () => {
|
|||
customAddNotes.user,
|
||||
customAddNotes.totalLeave,
|
||||
customAddNotes.dayAdditional,
|
||||
customAddNotes.specialLeave,
|
||||
customAddNotes.note,
|
||||
)
|
||||
}
|
||||
|
|
@ -377,77 +528,76 @@ const LeaveManagement = () => {
|
|||
Save
|
||||
</Button>
|
||||
</Drawer>
|
||||
<Box display={'flex'}>
|
||||
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
|
||||
<Box w="100%" display={'flex'}>
|
||||
<Select
|
||||
w="50%"
|
||||
value={date.year}
|
||||
size="xs"
|
||||
ml={'sm'}
|
||||
label="Year"
|
||||
data={Array.from({ length: 10 }, (_, index) => {
|
||||
return {
|
||||
value: (
|
||||
parseInt(moment(Date.now()).format('YYYY')) -
|
||||
3 +
|
||||
index
|
||||
).toString(),
|
||||
label: (
|
||||
parseInt(moment(Date.now()).format('YYYY')) -
|
||||
3 +
|
||||
index
|
||||
).toString(),
|
||||
disabled:
|
||||
parseInt(moment(Date.now()).format('YYYY')) - 3 + index >
|
||||
parseInt(moment(Date.now()).format('YYYY')),
|
||||
}
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setDate({ ...date, year: e! })
|
||||
}}
|
||||
></Select>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
w="70%"
|
||||
pl={200}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
|
||||
{/* Filter Year, Export Btn */}
|
||||
<Flex justify="space-between" align="flex-end">
|
||||
<Select
|
||||
value={date.year}
|
||||
size="xs"
|
||||
label="Year"
|
||||
data={Array.from({ length: 10 }, (_, index) => {
|
||||
return {
|
||||
value: (
|
||||
parseInt(moment(Date.now()).format('YYYY')) -
|
||||
3 +
|
||||
index
|
||||
).toString(),
|
||||
label: (
|
||||
parseInt(moment(Date.now()).format('YYYY')) -
|
||||
3 +
|
||||
index
|
||||
).toString(),
|
||||
disabled:
|
||||
parseInt(moment(Date.now()).format('YYYY')) - 3 + index >
|
||||
parseInt(moment(Date.now()).format('YYYY')),
|
||||
}
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setDate({ ...date, year: e! })
|
||||
}}
|
||||
w={200}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={handleExport}
|
||||
leftSection={<IconFileExcel size={16} />}
|
||||
>
|
||||
<Box display={'flex'} style={{ alignItems: 'end' }}>
|
||||
<Button
|
||||
size="xs"
|
||||
ml={'sm'}
|
||||
onClick={handleExport}
|
||||
leftSection={<IconFileExcel size={16} />}
|
||||
>
|
||||
Export Excel
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
Export Excel
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Leave Day Table */}
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
mt={'md'}
|
||||
miw={1580}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr bg={'#228be66b'}>
|
||||
<Table.Th ></Table.Th>
|
||||
<Table.Th ta={'center'} style={{ width: '40px' }}></Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
{monthInYear.map((d) => {
|
||||
const isCurrentMonth =
|
||||
Number(date.year) === currentYear && d.value === currentMonth
|
||||
return (
|
||||
<Menu width={200} shadow="md" key={d.value}>
|
||||
<Menu.Target>
|
||||
<Table.Th
|
||||
ta={'center'}
|
||||
style={{ cursor: 'pointer', width: '60px' }}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
width: '40px',
|
||||
backgroundColor: isCurrentMonth
|
||||
? '#F2E891'
|
||||
: undefined,
|
||||
color: isCurrentMonth ? '#000' : undefined,
|
||||
fontWeight: isCurrentMonth ? 'bold' : undefined,
|
||||
}}
|
||||
>
|
||||
<span>{d.name}</span>
|
||||
</Table.Th>
|
||||
|
|
@ -458,7 +608,7 @@ const LeaveManagement = () => {
|
|||
<Table.Th ta={'center'} style={{ width: '80px' }}>
|
||||
Total
|
||||
</Table.Th>
|
||||
<Table.Th ta={'center'} style={{ width: '80px' }}>
|
||||
<Table.Th ta={'center'} style={{ width: '130px' }}>
|
||||
Off
|
||||
</Table.Th>
|
||||
<Table.Th ta={'center'} style={{ width: '80px' }}>
|
||||
|
|
@ -472,23 +622,41 @@ const LeaveManagement = () => {
|
|||
{data.map((user, index) => {
|
||||
let totalDayOff = 0
|
||||
let totalDayLeave =
|
||||
user.leaveDay.ld_day + user.leaveDay.ld_date_additional
|
||||
user.leaveDay.ld_day_total +
|
||||
user.leaveDay.ld_additional_day +
|
||||
user.leaveDay.ld_special_leave_day
|
||||
let ld_day_total = user.leaveDay.ld_day_total
|
||||
let ld_additional_day = user.leaveDay.ld_additional_day
|
||||
let ld_special_leave_day = user.leaveDay.ld_special_leave_day
|
||||
let ld_note = user.leaveDay.ld_note
|
||||
let totalOnLeave = 0
|
||||
let totalLeaveWithoutPay = 0
|
||||
|
||||
return (
|
||||
<Table.Tr key={user.user.id} className={classes.tableTr}>
|
||||
<Table.Td ta={'center'}>{index + 1}</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip multiline label={user.user.name}>
|
||||
<div style={{display:'flex', alignItems:'center'}}><Avatar size={'md'} mr={'md'} src={import.meta.env.VITE_BACKEND_URL.includes('local')
|
||||
? import.meta.env.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
user.user.avatar
|
||||
: import.meta.env.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
user.user.avatar}/>{user.user.name}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar
|
||||
size={'md'}
|
||||
mr={'md'}
|
||||
src={
|
||||
import.meta.env.VITE_BACKEND_URL.includes('local')
|
||||
? import.meta.env.VITE_BACKEND_URL +
|
||||
'storage/' +
|
||||
user.user.avatar
|
||||
: import.meta.env.VITE_BACKEND_URL +
|
||||
'image/storage/' +
|
||||
user.user.avatar
|
||||
}
|
||||
/>
|
||||
{user.user.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
|
||||
{/* On leave per month */}
|
||||
{monthInYear.map((d, i) => {
|
||||
let leaveDataByMonth = getDetailLeaveDay(
|
||||
user.monthlyLeaveDays,
|
||||
|
|
@ -497,62 +665,220 @@ const LeaveManagement = () => {
|
|||
const monthData = leaveDataByMonth[d.value]
|
||||
let total = monthData ? monthData.leave_days : 0
|
||||
totalDayOff = totalDayOff + total
|
||||
|
||||
let onleaveDaysInMonth: MonthlyLeaveDays[] = []
|
||||
let nopayDaysInMonth: MonthlyLeaveDays[] = []
|
||||
let tempDaysInMonth: MonthlyLeaveDays[] = []
|
||||
|
||||
let totalOnLeaveMonth = 0
|
||||
let totalLeaveWithoutPayMonth = 0
|
||||
let totalTempMonth = 0
|
||||
let usedAdditionalDay = 0
|
||||
|
||||
user.monthlyLeaveDays
|
||||
.filter((item) => item.month === d.value)
|
||||
.map((item) => {
|
||||
if (item.reason_code === 'ONLEAVE') {
|
||||
totalOnLeaveMonth += Number(item.leave_days)
|
||||
onleaveDaysInMonth.push(item)
|
||||
} else if (item.reason_code === 'LEAVE_WITHOUT_PAY') {
|
||||
totalLeaveWithoutPayMonth += Number(item.leave_days)
|
||||
nopayDaysInMonth.push(item)
|
||||
} else {
|
||||
totalTempMonth += Number(item.leave_days)
|
||||
tempDaysInMonth.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Xử lý hiện thị phép tồn sử dụng
|
||||
let tmpTotalOnleave = totalOnLeave
|
||||
totalOnLeave += totalOnLeaveMonth
|
||||
|
||||
if (d.value < 4) {
|
||||
if (totalOnLeave < ld_additional_day) {
|
||||
usedAdditionalDay = totalOnLeaveMonth
|
||||
totalOnLeaveMonth = 0
|
||||
} else {
|
||||
usedAdditionalDay = ld_additional_day - tmpTotalOnleave
|
||||
if (usedAdditionalDay >= 0) {
|
||||
totalOnLeaveMonth -= usedAdditionalDay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalLeaveWithoutPay += totalLeaveWithoutPayMonth
|
||||
|
||||
return (
|
||||
<Table.Td
|
||||
bg={total > 0 ? '#ffb5b5' : ''}
|
||||
opacity={d.value > currentMonth ? 0.4 : 1}
|
||||
key={i}
|
||||
ta={'center'}
|
||||
>
|
||||
<Tooltip
|
||||
multiline
|
||||
label={user.monthlyLeaveDays
|
||||
.filter((item) => item.month === d.value)
|
||||
.map((itemDay, indexDay) => {
|
||||
return (
|
||||
<p key={indexDay}>
|
||||
- {itemDay.reason_name} (
|
||||
{itemDay.time_type_name}) {itemDay.day}/
|
||||
{itemDay.month}
|
||||
</p>
|
||||
)
|
||||
})}
|
||||
label={
|
||||
<Box p={4}>
|
||||
{usedAdditionalDay > 0 && (
|
||||
<Text fw={500} c="violet" size="sm" mb={4}>
|
||||
Phép tồn: {usedAdditionalDay}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{totalOnLeaveMonth > 0 && (
|
||||
<Box>
|
||||
<Text fw={500} c="teal" size="sm" mb={4}>
|
||||
Có phép: {totalOnLeaveMonth}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{totalOnLeaveMonth > 0 ||
|
||||
usedAdditionalDay > 0 ? (
|
||||
<Stack gap={2} pl="md">
|
||||
{onleaveDaysInMonth?.map(
|
||||
(itemDay: any, indexDay: number) => (
|
||||
<Text size="xs" key={indexDay}>
|
||||
• {itemDay.time_type_name} (
|
||||
{itemDay.day}/{itemDay.month})
|
||||
</Text>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{totalLeaveWithoutPayMonth > 0 && (
|
||||
<Box mt={6}>
|
||||
<Text fw={500} c="red" size="sm" mb={4}>
|
||||
Không phép: {totalLeaveWithoutPayMonth}
|
||||
</Text>
|
||||
<Stack gap={2} pl="md">
|
||||
{nopayDaysInMonth?.map(
|
||||
(itemDay: any, indexDay: number) => (
|
||||
<Text size="xs" key={indexDay}>
|
||||
• {itemDay.time_type_name} (
|
||||
{itemDay.day}/{itemDay.month})
|
||||
</Text>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{totalTempMonth > 0 && (
|
||||
<Box mt={6}>
|
||||
<Group align="center">
|
||||
<Text fw={500} c="blue" size="sm" mb={3}>
|
||||
Nghỉ dự kiến: {totalTempMonth}
|
||||
</Text>
|
||||
|
||||
<Tooltip
|
||||
multiline
|
||||
opened
|
||||
position="right"
|
||||
label={
|
||||
<Text size="xs">
|
||||
Ngày nghỉ đã được duyệt, sẽ tính phép
|
||||
khi đến tháng nghỉ.
|
||||
</Text>
|
||||
}
|
||||
offset={{ mainAxis: 15 }}
|
||||
>
|
||||
<Text fw={500} c="blue">
|
||||
<IconHelpCircle
|
||||
width={15}
|
||||
height={15}
|
||||
/>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<Stack gap={2} pl="md">
|
||||
{tempDaysInMonth?.map(
|
||||
(itemDay: any, indexDay: number) => (
|
||||
<Text size="xs" key={indexDay}>
|
||||
• {itemDay.time_type_name} (
|
||||
{itemDay.day}/{itemDay.month})
|
||||
</Text>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<p>{total === 0 ? '' : total}</p>
|
||||
<Text size="sm">{total === 0 ? '' : total}</Text>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
)
|
||||
})}
|
||||
|
||||
<Table.Td
|
||||
ta={'center'}
|
||||
bg={totalDayLeave > 0 ? '#92e6f2' : ''}
|
||||
>
|
||||
{totalDayLeave}
|
||||
{/* Total */}
|
||||
<Table.Td ta={'center'} bg="#92e6f2">
|
||||
<Tooltip
|
||||
multiline
|
||||
label={showAllTotal(
|
||||
ld_day_total,
|
||||
ld_additional_day,
|
||||
ld_special_leave_day,
|
||||
)}
|
||||
>
|
||||
<Text size="sm">{totalDayLeave}</Text>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
<Table.Td ta={'center'} bg={totalDayOff > 0 ? '#ffb5b5' : ''}>
|
||||
|
||||
{/* Off */}
|
||||
<Table.Td>
|
||||
{totalDayOff > 0 ? (
|
||||
<Tooltip
|
||||
multiline
|
||||
label={showAllOff(user.monthlyLeaveDays)}
|
||||
>
|
||||
<p> {totalDayOff}</p>
|
||||
<Box>
|
||||
<Flex justify="space-between" mb="xs" align="center">
|
||||
<Text size="sm">Có phép: </Text>
|
||||
<Text
|
||||
size="sm"
|
||||
bg="#c3ffc3"
|
||||
fw="bold"
|
||||
p={5}
|
||||
style={{ borderRadius: 5 }}
|
||||
>
|
||||
{totalOnLeave}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text size="sm">Không phép: </Text>
|
||||
<Text
|
||||
size="sm"
|
||||
bg="#ffb5b5"
|
||||
fw="bold"
|
||||
p={5}
|
||||
style={{ borderRadius: 5 }}
|
||||
>
|
||||
{totalLeaveWithoutPay}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<></>
|
||||
''
|
||||
)}
|
||||
</Table.Td>
|
||||
|
||||
{/* Remaining */}
|
||||
<Table.Td
|
||||
ta={'center'}
|
||||
bg={
|
||||
totalDayLeave - totalDayOff == 0
|
||||
? ''
|
||||
: totalDayLeave - totalDayOff > 0
|
||||
? '#c3ffc3'
|
||||
: '#ffb5b5'
|
||||
}
|
||||
bg={totalDayLeave - totalOnLeave > 0 ? '#b5cafb' : ''}
|
||||
>
|
||||
{totalDayLeave - totalDayOff}
|
||||
<Text size="sm">{totalDayLeave - totalOnLeave}</Text>
|
||||
</Table.Td>
|
||||
|
||||
{/* Note */}
|
||||
<Table.Td>
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -571,13 +897,18 @@ const LeaveManagement = () => {
|
|||
</Text>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Textarea size="sm" autosize>
|
||||
{ld_note}
|
||||
</Textarea>
|
||||
<Textarea
|
||||
size="sm"
|
||||
autosize
|
||||
value={ld_note}
|
||||
readOnly
|
||||
/>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Box>
|
||||
</Table.Td>
|
||||
|
||||
{/* Action */}
|
||||
<Table.Td ta={'center'}>
|
||||
<IconEdit
|
||||
color="green"
|
||||
|
|
@ -585,13 +916,17 @@ const LeaveManagement = () => {
|
|||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
let totalLeave =
|
||||
user.leaveDay.ld_day == 0
|
||||
user.leaveDay.ld_day_total == 0
|
||||
? ''
|
||||
: String(user.leaveDay.ld_day)
|
||||
: String(user.leaveDay.ld_day_total)
|
||||
let dayAdditional =
|
||||
user.leaveDay.ld_date_additional == 0
|
||||
user.leaveDay.ld_additional_day == 0
|
||||
? ''
|
||||
: String(user.leaveDay.ld_date_additional)
|
||||
: String(user.leaveDay.ld_additional_day)
|
||||
let specialLeave =
|
||||
user.leaveDay.ld_special_leave_day == 0
|
||||
? ''
|
||||
: String(user.leaveDay.ld_special_leave_day)
|
||||
open1()
|
||||
setCustomAddNotes({
|
||||
...customAddNotes,
|
||||
|
|
@ -599,6 +934,7 @@ const LeaveManagement = () => {
|
|||
note: ld_note,
|
||||
totalLeave: totalLeave,
|
||||
dayAdditional: dayAdditional,
|
||||
specialLeave: specialLeave,
|
||||
user: {
|
||||
id: user.user.id,
|
||||
name: user.user.name,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,42 @@
|
|||
export const PageNotFound = () => {
|
||||
return <>{'Not found!'}</>
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Image,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import image404 from '../../../public/404Image.jpg'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const PageNotFound = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Container mt="lg">
|
||||
<SimpleGrid spacing={{ base: 40, sm: 80 }} cols={{ base: 1, sm: 2 }}>
|
||||
<Box py="3rem">
|
||||
<Title> Something is not right...</Title>
|
||||
<Text c="dimmed" size="lg">
|
||||
Page you are trying to open does not exist. You may have mistyped
|
||||
the address, or the page has been moved to another URL. If you think
|
||||
this is an error contact support.
|
||||
</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
mt="xl"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
Get back to home page
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Image src={image404} />
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageNotFound
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
getTicketsOfUser,
|
||||
} from '@/api/Admin'
|
||||
import { DataTablePagination } from '@/components/DataTable/DataTable'
|
||||
import { Xdelete, create } from '@/rtk/helpers/CRUD'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
import { get, post } from '@/rtk/helpers/apiService'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
|
|
@ -26,6 +26,7 @@ import { IconTrash } from '@tabler/icons-react'
|
|||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import classes from './Tickets.module.css'
|
||||
import { _NOTIFICATION_MESS } from '@/rtk/helpers/notificationMess'
|
||||
|
||||
type TTickets = {
|
||||
id: number
|
||||
|
|
@ -78,10 +79,16 @@ const Tickets = () => {
|
|||
const [item, setItem] = useState({ id: 0 })
|
||||
const [activeBtn, setActiveBtn] = useState(false)
|
||||
const [disableBtn, setDisableBtn] = useState(false)
|
||||
const [isFetchData, setIsFetch] = useState(false)
|
||||
|
||||
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
|
||||
const [dataReason, setDataReason] = useState<DataReason[]>([])
|
||||
|
||||
const [confirmModal, setConfirmModal] = useState(false)
|
||||
const [confirmMessage, setConfirmMessage] = useState('')
|
||||
const [confirmValues, setConfirmValues] = useState<TTickets | null>(null)
|
||||
const [confirmLoading, setConfirmLoading] = useState(false)
|
||||
|
||||
const getListMasterByType = async (type: string) => {
|
||||
try {
|
||||
const params = {
|
||||
|
|
@ -103,6 +110,8 @@ const Tickets = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsFetch(true)
|
||||
|
||||
const resultTimeType = await getListMasterByType('TIME_TYPE')
|
||||
setDataTimeType(
|
||||
resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
|
||||
|
|
@ -110,6 +119,8 @@ const Tickets = () => {
|
|||
|
||||
const resultReason = await getListMasterByType('REASON')
|
||||
setDataReason(resultReason)
|
||||
|
||||
setIsFetch(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
|
@ -271,27 +282,61 @@ const Tickets = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleCreate = async (values: TTickets) => {
|
||||
const handleCreate = async (values: TTickets, confirm: boolean = false) => {
|
||||
try {
|
||||
const res = await create(
|
||||
addTicket,
|
||||
{
|
||||
// time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
|
||||
start_date: moment(values.start_date).format('YYYY-MM-DD'),
|
||||
start_period: values.start_period,
|
||||
end_date: moment(values.end_date).format('YYYY-MM-DD'),
|
||||
end_period: values.end_period,
|
||||
type: values.type,
|
||||
reason: values.reason,
|
||||
},
|
||||
getAllTickets,
|
||||
)
|
||||
if (res === true) {
|
||||
let params = {
|
||||
// time_string: moment(values.time_string).format('YYYY-MM-DD HH:mm:ss'),
|
||||
start_date: moment(values.start_date).format('YYYY-MM-DD'),
|
||||
start_period: values.start_period,
|
||||
end_date: moment(values.end_date).format('YYYY-MM-DD'),
|
||||
end_period: values.end_period,
|
||||
type: values.type,
|
||||
reason: values.reason,
|
||||
is_accept: confirm,
|
||||
}
|
||||
|
||||
let res = await post(addTicket, params)
|
||||
|
||||
if (res.status) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: _NOTIFICATION_MESS.create_success,
|
||||
color: 'green',
|
||||
})
|
||||
setAction('')
|
||||
form.reset()
|
||||
|
||||
getAllTickets()
|
||||
}
|
||||
if (!res.status && res.errors) {
|
||||
if (!res.data?.success && res.data?.message) {
|
||||
//popup notification confirm or cancel
|
||||
setConfirmMessage(res.data?.message)
|
||||
setConfirmValues(values)
|
||||
setConfirmModal(true)
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: (
|
||||
<div style={{ whiteSpace: 'pre-line' }}>
|
||||
{res.message ?? _NOTIFICATION_MESS.create_error}
|
||||
</div>
|
||||
),
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(res, 'res')
|
||||
// return res.status
|
||||
} catch (error: any) {
|
||||
if (error.response.status === 422) {
|
||||
const errorMess = error.response.data.message
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: errorMess,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,15 +367,14 @@ const Tickets = () => {
|
|||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
<h3>
|
||||
Tickets
|
||||
</h3>
|
||||
<h3>Tickets</h3>
|
||||
<Button
|
||||
m={5}
|
||||
onClick={() => {
|
||||
setAction('add')
|
||||
form.reset()
|
||||
}}
|
||||
disabled={isFetchData}
|
||||
>
|
||||
+ Add
|
||||
</Button>
|
||||
|
|
@ -490,6 +534,57 @@ const Tickets = () => {
|
|||
</Group>
|
||||
</Text>
|
||||
</Dialog>
|
||||
|
||||
{/* Confirm Modal */}
|
||||
<Modal
|
||||
opened={confirmModal}
|
||||
onClose={() => !confirmLoading && setConfirmModal(false)}
|
||||
title={
|
||||
<Text fw={700} fz="lg">
|
||||
Warning
|
||||
</Text>
|
||||
}
|
||||
centered
|
||||
closeOnClickOutside={!confirmLoading}
|
||||
closeOnEscape={!confirmLoading}
|
||||
>
|
||||
<Box p="md">
|
||||
<Text style={{ whiteSpace: 'pre-line' }} mb={20}>
|
||||
{confirmMessage}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button
|
||||
color="green"
|
||||
loading={confirmLoading}
|
||||
onClick={async () => {
|
||||
if (confirmValues) {
|
||||
try {
|
||||
setConfirmLoading(true)
|
||||
action === 'add' &&
|
||||
(await handleCreate(confirmValues, true))
|
||||
setConfirmLoading(false)
|
||||
setConfirmModal(false)
|
||||
} catch (error) {
|
||||
setConfirmLoading(false)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
disabled={confirmLoading}
|
||||
onClick={() => {
|
||||
setConfirmModal(false)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,4 +45,38 @@
|
|||
|
||||
.dialogText {
|
||||
color: light-dark(#2d353c, white);
|
||||
}
|
||||
}
|
||||
|
||||
/* Thêm styles cho Modal xác nhận xóa */
|
||||
.deleteModal {
|
||||
background-color: light-dark(white, #2d353c);
|
||||
text-align: center;
|
||||
border: solid 1px #ff4646;
|
||||
}
|
||||
|
||||
.deleteModalTitle {
|
||||
color: #ff4646;
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.deleteModalContent {
|
||||
color: light-dark(#2d353c, white);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.deleteModalFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background-color: #ff4646;
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background-color: #ff6b6b;
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import { getListMaster, getTickets, handleTicket } from '@/api/Admin'
|
||||
import {
|
||||
getListMaster,
|
||||
getTickets,
|
||||
handleTicket,
|
||||
updateTicket,
|
||||
} from '@/api/Admin'
|
||||
import { DataTablePagination } from '@/components/DataTable/DataTable'
|
||||
import { create } from '@/rtk/helpers/CRUD'
|
||||
import { create, update } from '@/rtk/helpers/CRUD'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -9,12 +14,13 @@ import {
|
|||
HoverCard,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
Text,
|
||||
Textarea,
|
||||
} from '@mantine/core'
|
||||
import { useForm } from '@mantine/form'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { IconCheckbox, IconSquareXFilled } from '@tabler/icons-react'
|
||||
import { IconCheckbox, IconEdit, IconSquareXFilled } from '@tabler/icons-react'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import classes from './TicketsManagement.module.css'
|
||||
|
|
@ -24,6 +30,7 @@ type TTickets = {
|
|||
ticket_id: number
|
||||
admin_note: string
|
||||
action: string
|
||||
status: string
|
||||
}
|
||||
|
||||
type TListTickets = {
|
||||
|
|
@ -78,6 +85,7 @@ const TicketsManagement = () => {
|
|||
end_period: '',
|
||||
reason: '',
|
||||
type: '',
|
||||
status: '',
|
||||
})
|
||||
const [disableBtn, setDisableBtn] = useState(false)
|
||||
const [filter, setFilter] = useState({
|
||||
|
|
@ -86,6 +94,7 @@ const TicketsManagement = () => {
|
|||
})
|
||||
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
|
||||
const [dataReason, setDataReason] = useState<DataReason[]>([])
|
||||
const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState<boolean>(false)
|
||||
|
||||
const getListMasterByType = async (type: string) => {
|
||||
try {
|
||||
|
|
@ -156,7 +165,7 @@ const TicketsManagement = () => {
|
|||
},
|
||||
{
|
||||
name: 'reason',
|
||||
size: '15%',
|
||||
size: '10%',
|
||||
header: 'Notes',
|
||||
render: (row: any) => {
|
||||
return (
|
||||
|
|
@ -198,7 +207,7 @@ const TicketsManagement = () => {
|
|||
},
|
||||
{
|
||||
name: 'admin_note',
|
||||
size: '15%',
|
||||
size: '10%',
|
||||
header: 'Admin Notes',
|
||||
render: (row: any) => {
|
||||
return (
|
||||
|
|
@ -226,6 +235,18 @@ const TicketsManagement = () => {
|
|||
size: '10%',
|
||||
header: 'Updated By',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
size: '10%',
|
||||
header: 'Updated At',
|
||||
render: (row: any) => {
|
||||
if (row?.updated_at) {
|
||||
return (
|
||||
<Box>{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '#',
|
||||
size: '5%',
|
||||
|
|
@ -254,7 +275,23 @@ const TicketsManagement = () => {
|
|||
height={20}
|
||||
/>
|
||||
</Box>
|
||||
) : null
|
||||
) : (
|
||||
<Box className={classes.optionIcon}>
|
||||
<IconEdit
|
||||
className={classes.editIcon}
|
||||
onClick={() => {
|
||||
setAction('update')
|
||||
setItem(row)
|
||||
form.reset()
|
||||
console.log(row)
|
||||
form.setFieldValue('status', row.status)
|
||||
form.setFieldValue('admin_note', row.admin_note)
|
||||
}}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -347,6 +384,27 @@ const TicketsManagement = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async (values: TTickets) => {
|
||||
try {
|
||||
const res = await update(
|
||||
updateTicket + `?ticket_id=${item?.id}`,
|
||||
{
|
||||
ticket_id: item.id,
|
||||
admin_note: values.admin_note,
|
||||
status: values.status,
|
||||
},
|
||||
getAllTickets,
|
||||
)
|
||||
if (res === true) {
|
||||
setAction('')
|
||||
setIsRefuseConfirmOpen(false)
|
||||
form.reset()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllTickets()
|
||||
}, [filter])
|
||||
|
|
@ -356,6 +414,7 @@ const TicketsManagement = () => {
|
|||
ticket_id: 0,
|
||||
action: '',
|
||||
admin_note: '',
|
||||
status: '',
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -416,21 +475,38 @@ const TicketsManagement = () => {
|
|||
</Box>
|
||||
|
||||
<Modal
|
||||
opened={action === 'confirm' || action === 'refuse'}
|
||||
opened={
|
||||
action === 'confirm' || action === 'refuse' || action === 'update'
|
||||
}
|
||||
onClose={() => {
|
||||
setAction('')
|
||||
setIsRefuseConfirmOpen(false)
|
||||
form.reset()
|
||||
}}
|
||||
title={
|
||||
<Text pl={'sm'} fw={700} fz={'lg'}>
|
||||
{action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'}
|
||||
{action === 'confirm'
|
||||
? 'Confirm Ticket'
|
||||
: action === 'refuse'
|
||||
? 'Refuse Ticket'
|
||||
: 'Update Ticket'}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
setDisableBtn(true)
|
||||
await handleSave(values)
|
||||
|
||||
if (action === 'update') {
|
||||
if (values.status === 'REFUSED') {
|
||||
setIsRefuseConfirmOpen(true)
|
||||
} else {
|
||||
await handleUpdate(values)
|
||||
}
|
||||
} else {
|
||||
await handleSave(values)
|
||||
}
|
||||
|
||||
setDisableBtn(false)
|
||||
})}
|
||||
>
|
||||
|
|
@ -509,14 +585,39 @@ const TicketsManagement = () => {
|
|||
|
||||
<Textarea
|
||||
label="Admin Notes"
|
||||
// required
|
||||
value={form.values.admin_note}
|
||||
onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
|
||||
/>
|
||||
|
||||
{action === 'update' && item.status !== 'REFUSED' ? (
|
||||
<Switch
|
||||
mt="md"
|
||||
color="red"
|
||||
label={
|
||||
<Text size="sm">
|
||||
Change status to{' '}
|
||||
<Text span color="red" fw="bold">
|
||||
Refuse
|
||||
</Text>
|
||||
</Text>
|
||||
}
|
||||
style={{ width: 'fit-content' }}
|
||||
checked={form.values.status === 'REFUSED'}
|
||||
onChange={(event) =>
|
||||
form.setFieldValue(
|
||||
'status',
|
||||
event.currentTarget.checked ? 'REFUSED' : 'CONFIRMED',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
<Box ta={'center'}>
|
||||
<Button
|
||||
mt={'lg'}
|
||||
bg={'green'}
|
||||
bg={action === 'update' ? 'blue' : 'green'}
|
||||
type="submit"
|
||||
disabled={disableBtn}
|
||||
>
|
||||
|
|
@ -526,6 +627,50 @@ const TicketsManagement = () => {
|
|||
</Box>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
opened={isRefuseConfirmOpen}
|
||||
onClose={() => {
|
||||
setIsRefuseConfirmOpen(false)
|
||||
}}
|
||||
centered
|
||||
size="sm"
|
||||
classNames={{
|
||||
content: classes.deleteModal,
|
||||
}}
|
||||
>
|
||||
<Text className={classes.deleteModalTitle}>Confirm Update</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
Changing ticket status to <strong>Refused</strong> will also delete
|
||||
all related notes.
|
||||
</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
Are you sure you want to proceed?
|
||||
</Text>
|
||||
|
||||
<Box className={classes.deleteModalFooter}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsRefuseConfirmOpen(false)
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.deleteButton}
|
||||
onClick={async () => {
|
||||
setDisableBtn(true)
|
||||
await handleUpdate(form.values)
|
||||
setDisableBtn(false)
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,3 +60,37 @@
|
|||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Thêm styles cho Modal xác nhận xóa */
|
||||
.deleteModal {
|
||||
background-color: light-dark(white, #2d353c);
|
||||
text-align: center;
|
||||
border: solid 1px #ff4646;
|
||||
}
|
||||
|
||||
.deleteModalTitle {
|
||||
color: #ff4646;
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.deleteModalContent {
|
||||
color: light-dark(#2d353c, white);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.deleteModalFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background-color: #ff4646;
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background-color: #ff6b6b;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@ const Timekeeping = () => {
|
|||
const [exportModalOpened, setExportModalOpened] = useState(false)
|
||||
const [exportOption, setExportOption] = useState('default')
|
||||
|
||||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false)
|
||||
const [isDisableDeleteBtn, setIsDisableDeleteBtn] = useState(false)
|
||||
const [noteToDelete, setNoteToDelete] = useState<any>(null)
|
||||
|
||||
const getListMasterByType = async (type: string) => {
|
||||
try {
|
||||
const params = {
|
||||
|
|
@ -445,16 +449,16 @@ const Timekeeping = () => {
|
|||
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,
|
||||
option: option
|
||||
option: option,
|
||||
},
|
||||
fileName
|
||||
fileName,
|
||||
)
|
||||
setExportModalOpened(false)
|
||||
} catch (error) {
|
||||
|
|
@ -462,6 +466,64 @@ const Timekeeping = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (noteToDelete) {
|
||||
setIsDisableDeleteBtn(true)
|
||||
await handleDelete(noteToDelete.id)
|
||||
setIsDisableDeleteBtn(false)
|
||||
setIsDeleteConfirmOpen(false)
|
||||
setNoteToDelete(null)
|
||||
}
|
||||
}
|
||||
|
||||
const openDeleteConfirm = (note: any) => {
|
||||
setNoteToDelete(note)
|
||||
setIsDeleteConfirmOpen(true)
|
||||
}
|
||||
|
||||
const DeleteConfirmModal = () => (
|
||||
<Modal
|
||||
opened={isDeleteConfirmOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteConfirmOpen(false)
|
||||
setNoteToDelete(null)
|
||||
}}
|
||||
centered
|
||||
size="sm"
|
||||
classNames={{
|
||||
content: classes.deleteModal,
|
||||
}}
|
||||
>
|
||||
<Text className={classes.deleteModalTitle}>Confirm Delete</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
This action will change the ticket status to <strong>Refused</strong>{' '}
|
||||
and delete all related notes.
|
||||
</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
Are you sure you want to proceed?
|
||||
</Text>
|
||||
<Box className={classes.deleteModalFooter}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsDeleteConfirmOpen(false)
|
||||
setNoteToDelete(null)
|
||||
}}
|
||||
disabled={isDisableDeleteBtn}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.deleteButton}
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={isDisableDeleteBtn}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
|
|
@ -631,11 +693,7 @@ const Timekeeping = () => {
|
|||
>
|
||||
<IconTrash
|
||||
className={classes.deleteIcon}
|
||||
onClick={async () => {
|
||||
await handleDelete(item.id)
|
||||
// handleUpdateCacheMonth()
|
||||
// close2()
|
||||
}}
|
||||
onClick={() => openDeleteConfirm(item)}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
|
|
@ -655,7 +713,7 @@ const Timekeeping = () => {
|
|||
data={Array.from({ length: 12 }, (_, index) => {
|
||||
return {
|
||||
value: (1 + index).toString(),
|
||||
label: (1 + index).toString()
|
||||
label: (1 + index).toString(),
|
||||
}
|
||||
})}
|
||||
onChange={(e) => {
|
||||
|
|
@ -710,9 +768,9 @@ const Timekeeping = () => {
|
|||
Save
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
onClick={() => setExportModalOpened(true)}
|
||||
size="xs"
|
||||
<Button
|
||||
onClick={() => setExportModalOpened(true)}
|
||||
size="xs"
|
||||
ml="md"
|
||||
bg={'green'}
|
||||
leftSection={<IconFileExcel size={16} />}
|
||||
|
|
@ -1074,20 +1132,19 @@ const Timekeeping = () => {
|
|||
placeholder="Choose an option"
|
||||
value={exportOption}
|
||||
onChange={(value) => setExportOption(value || 'default')}
|
||||
data={[
|
||||
{ value: 'default', label: 'Default' }
|
||||
]}
|
||||
data={[{ value: 'default', label: 'Default' }]}
|
||||
mb="md"
|
||||
/>
|
||||
<Box style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
|
||||
<Box
|
||||
style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}
|
||||
>
|
||||
<Button variant="outline" onClick={() => setExportModalOpened(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={() => handleExport(exportOption)}>
|
||||
Export
|
||||
</Button>
|
||||
<Button onClick={() => handleExport(exportOption)}>Export</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
<DeleteConfirmModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,3 +45,37 @@
|
|||
.dialogText {
|
||||
color: light-dark(#2d353c, white);
|
||||
}
|
||||
|
||||
/* Thêm styles cho Modal xác nhận xóa */
|
||||
.deleteModal {
|
||||
background-color: light-dark(white, #2d353c);
|
||||
text-align: center;
|
||||
border: solid 1px rgb(9, 132, 132);
|
||||
}
|
||||
|
||||
.deleteModalTitle {
|
||||
color: rgb(9, 132, 132);
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.deleteModalContent {
|
||||
color: light-dark(#2d353c, white);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.deleteModalFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background-color: rgb(9, 132, 132);
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background-color: rgb(9, 132, 132);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Group,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core'
|
||||
|
|
@ -24,28 +25,31 @@ const UsersManagement = () => {
|
|||
const [users, setUsers] = useState<TUser[]>([])
|
||||
const [action, setAction] = useState('')
|
||||
const [activeBtn, setActiveBtn] = useState(false)
|
||||
const [item, setItem] = useState({ id: 0 })
|
||||
const [item, setItem] = useState({ id: 0, is_permanent: false })
|
||||
const [disableBtn, setDisableBtn] = useState(false)
|
||||
const [info, setInfo] = useState('')
|
||||
const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] =
|
||||
useState<boolean>(false)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'id',
|
||||
size: '3%',
|
||||
size: '5%',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
size: '17%',
|
||||
size: '20%',
|
||||
header: 'Name',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
size: '26%',
|
||||
size: '25%',
|
||||
header: 'Email',
|
||||
},
|
||||
{
|
||||
name: 'permission',
|
||||
size: '10%',
|
||||
size: '20%',
|
||||
header: 'Permission',
|
||||
render: (row: TUser) => {
|
||||
if (row.permission.includes(',')) {
|
||||
|
|
@ -57,9 +61,21 @@ const UsersManagement = () => {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'is_permanent',
|
||||
size: '20%',
|
||||
header: 'Employment Type',
|
||||
render: (row: TUser) => {
|
||||
return row.is_permanent ? (
|
||||
<Badge color="teal">Permanent</Badge>
|
||||
) : (
|
||||
<Badge color="violet">Probation</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '#',
|
||||
size: '5%',
|
||||
size: '10%',
|
||||
header: 'Action',
|
||||
render: (row: TUser) => {
|
||||
return (
|
||||
|
|
@ -68,6 +84,8 @@ const UsersManagement = () => {
|
|||
className={classes.editIcon}
|
||||
onClick={() => {
|
||||
setAction('edit')
|
||||
setItem(row)
|
||||
form.reset()
|
||||
form.setValues(row)
|
||||
}}
|
||||
width={20}
|
||||
|
|
@ -94,6 +112,7 @@ const UsersManagement = () => {
|
|||
name: '',
|
||||
email: '',
|
||||
permission: '',
|
||||
is_permanent: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -128,6 +147,7 @@ const UsersManagement = () => {
|
|||
const res = await update(createOrUpdateUser, values, getAll)
|
||||
if (res === true) {
|
||||
setAction('')
|
||||
setIsPermanentConfirmOpen(false)
|
||||
form.reset()
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -172,6 +192,7 @@ const UsersManagement = () => {
|
|||
opened={action === 'add' || action === 'edit'}
|
||||
onClose={() => {
|
||||
setAction('')
|
||||
setIsPermanentConfirmOpen(false)
|
||||
form.reset()
|
||||
}}
|
||||
title={
|
||||
|
|
@ -183,9 +204,15 @@ const UsersManagement = () => {
|
|||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
setDisableBtn(true)
|
||||
action === 'edit'
|
||||
? await handleUpdate(values)
|
||||
: await handleCreate(values)
|
||||
if (action === 'edit') {
|
||||
if (values.is_permanent && !item.is_permanent) {
|
||||
setIsPermanentConfirmOpen(true)
|
||||
} else {
|
||||
await handleUpdate(values)
|
||||
}
|
||||
} else {
|
||||
await handleCreate(values)
|
||||
}
|
||||
setDisableBtn(false)
|
||||
})}
|
||||
>
|
||||
|
|
@ -196,6 +223,7 @@ const UsersManagement = () => {
|
|||
value={form.values.name}
|
||||
error={form.errors.name}
|
||||
onChange={(e) => form.setFieldValue('name', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
|
|
@ -204,6 +232,7 @@ const UsersManagement = () => {
|
|||
value={form.values.email}
|
||||
error={form.errors.email}
|
||||
onChange={(e) => form.setFieldValue('email', e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
|
|
@ -224,7 +253,25 @@ const UsersManagement = () => {
|
|||
e!.filter((p) => p.trim() !== '').join(','),
|
||||
)
|
||||
}
|
||||
mb={'md'}
|
||||
/>
|
||||
|
||||
{action === 'edit' && !item.is_permanent ? (
|
||||
<Switch
|
||||
label="Permanent employee"
|
||||
style={{ width: 'fit-content' }}
|
||||
checked={form.values.is_permanent}
|
||||
onChange={(event) =>
|
||||
form.setFieldValue(
|
||||
'is_permanent',
|
||||
event.currentTarget.checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
<Box ta={'center'}>
|
||||
{action === 'add' ? (
|
||||
<Button
|
||||
|
|
@ -273,6 +320,50 @@ const UsersManagement = () => {
|
|||
{info}
|
||||
</Code>
|
||||
</Modal>
|
||||
|
||||
{/* Confirm change to permanent employee */}
|
||||
<Modal
|
||||
opened={isPermanentConfirmOpen}
|
||||
onClose={() => setIsPermanentConfirmOpen(false)}
|
||||
centered
|
||||
size="sm"
|
||||
classNames={{
|
||||
content: classes.deleteModal,
|
||||
}}
|
||||
>
|
||||
<Text className={classes.deleteModalTitle}>Confirm Update</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
This action will change the employment type from{' '}
|
||||
<strong>Probation</strong> to <strong>Permanent</strong>.
|
||||
</Text>
|
||||
<Text className={classes.deleteModalContent}>
|
||||
Are you sure you want to proceed?
|
||||
</Text>
|
||||
|
||||
<Box className={classes.deleteModalFooter}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsPermanentConfirmOpen(false)
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.deleteButton}
|
||||
onClick={async () => {
|
||||
setDisableBtn(true)
|
||||
await handleUpdate(form.values)
|
||||
setDisableBtn(false)
|
||||
}}
|
||||
disabled={disableBtn}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
<Dialog
|
||||
className={classes.dialog}
|
||||
opened={action === 'delete'}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const create = async (
|
|||
if (res.status === false) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: res.message ?? _NOTIFICATION_MESS.create_error,
|
||||
message: <div style={{ whiteSpace: 'pre-line' }}>{res.message ?? _NOTIFICATION_MESS.create_error}</div>,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export type TUser = {
|
|||
email: string
|
||||
name: string
|
||||
permission: string
|
||||
is_permanent: boolean
|
||||
}
|
||||
|
||||
export type DataReason = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue