diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/CategoryController.php b/BACKEND/Modules/Admin/app/Http/Controllers/CategoryController.php index 58c767c..0150d30 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/CategoryController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/CategoryController.php @@ -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; + } } diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index 477b056..a8b1863 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -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; }); diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php index 1e3e27b..2df8e81 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php @@ -2,9 +2,7 @@ namespace Modules\Admin\app\Http\Controllers; -use App\Helper\Cache\CurrentMonthTimekeeping; use App\Http\Controllers\Controller; -use App\Mail\ContactMail; use App\Mail\TicketMail; use App\Models\Notes; use App\Traits\AnalyzeData; @@ -20,6 +18,9 @@ use Modules\Admin\app\Models\Admin; use Modules\Admin\app\Models\Category; use Modules\Admin\app\Models\Ticket; use Modules\Admin\app\Models\Tracking; +use Illuminate\Support\Facades\Log; +use App\Models\LeaveDays; +use App\Models\Admin as UserModel; class TicketController extends Controller { @@ -101,7 +102,6 @@ class TicketController extends Controller ->paginate($request->get('per_page'))->toArray(), ['status' => true] ); - return response()->json($responseData); } @@ -191,7 +191,7 @@ class TicketController extends Controller public function createTicket(Request $request) { - // Define validation rules + // Validate input $rules = [ 'start_date' => 'required|date', 'start_period' => 'required|string', @@ -199,34 +199,146 @@ class TicketController extends Controller 'end_period' => 'required|string', 'type' => 'required|string', ]; - - // Validate the request $request->validate($rules); - // return $request; - //Get data from request - $startDate = $request->input('start_date'); //Start day - $startPeriod = $request->input('start_period'); //The session begins - $endDate = $request->input('end_date'); //End date - $endPeriod = $request->input('end_period'); //Session ends + // Get input data + $startDate = $request->input('start_date'); + $startPeriod = $request->input('start_period'); + $endDate = $request->input('end_date'); + $endPeriod = $request->input('end_period'); $type = $request->input('type'); $reason = $request->input('reason'); - $user = auth('admins')->user(); // user create ticket - - // return $user; - - $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod); - $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod); - $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type); - - $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y'); - $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y'); - + $isAccept = $request->input('is_accept') ?? false; $user = auth('admins')->user(); + + $start_date = Carbon::create($startDate)->setTimezone(env('TIME_ZONE')); + $end_date = Carbon::create($endDate)->setTimezone(env('TIME_ZONE')); + + $dataListPeriod = $this->getAllPeriodNew($start_date, $startPeriod, $end_date, $endPeriod); + if (empty($dataListPeriod)) { + return AbstractController::ResultError('Không thể tính toán khoảng thời gian nghỉ hợp lệ.'); + } + + // Lấy các ticket của user trong thời gian request + $userTickets = Ticket::where('user_id', $user->id) + ->whereIn('status', ['WAITING', 'CONFIRMED']) + ->where(function ($query) use ($start_date, $end_date) { + $query->where(function ($q) use ($start_date, $end_date) { + // Trường hợp 1: start_date nằm trong khoảng + $q->whereBetween(DB::raw('DATE(start_date)'), [$start_date->toDateString(), $end_date->toDateString()]); + }) + ->orWhere(function ($q) use ($start_date, $end_date) { + // Trường hợp 2: end_date nằm trong khoảng + $q->whereBetween(DB::raw('DATE(end_date)'), [$start_date->toDateString(), $end_date->toDateString()]); + }) + ->orWhere(function ($q) use ($start_date, $end_date) { + // Trường hợp 3: Khoảng thời gian được chọn nằm trong khoảng của ticket + $q->where(DB::raw('DATE(start_date)'), '<=', $start_date->toDateString()) + ->where(DB::raw('DATE(end_date)'), '>=', $end_date->toDateString()); + }); + }) + ->get(); + $userTicketListPeriod = []; + if ($userTickets->count() > 0) { + foreach ($userTickets as $ticket) { + $userTicketListPeriod = array_merge($userTicketListPeriod, $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period)); + } + } + + // Kiểm tra ticket tạo có trùng thời gian với các ticket cũ + $periodStrings = []; + $userTicketPeriodStrings = []; + foreach ($dataListPeriod as $period) { + if ($period['period'] == 'ALL') { + $periodStrings[] = $period['date'] . '_S'; + $periodStrings[] = $period['date'] . '_C'; + continue; + } + + $periodStrings[] = $period['date'] . '_' . $period['period']; + } + foreach ($userTicketListPeriod as $period) { + if ($period['period'] == 'ALL') { + $userTicketPeriodStrings[] = $period['date'] . '_S'; + $userTicketPeriodStrings[] = $period['date'] . '_C'; + continue; + } + + $userTicketPeriodStrings[] = $period['date'] . '_' . $period['period']; + } + + if (count(array_intersect($periodStrings, $userTicketPeriodStrings)) > 0) { + return AbstractController::ResultError('Đã có ticket được tạo trong thời gian này, không thể tạo ticket mới!'); + } + + // Kiểm tra khi type = ONLEAVE (nghỉ phép) + if ($type === 'ONLEAVE' && !$isAccept) { + // Lấy tickets nghỉ phép đang ở trạng thái WAITING + $ticketsWaiting = Ticket::where('user_id', $user->id)->where('status', 'WAITING')->where('type', 'ONLEAVE') + ->get(); + $dataListPeriodWaiting = []; + if ($ticketsWaiting->count() > 0) { + foreach ($ticketsWaiting as $ticket) { + $dataListPeriodWaiting = array_merge($dataListPeriodWaiting, $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period)); + } + } + + // Tạo thông báo về tickets waiting nếu có + $waitingTicketsMessage = ''; + if (!empty($dataListPeriodWaiting)) { + // Kiểm tra số dư ngày phép cho tickets waiting + $waitingTicketsMessage = "Bạn đang có " . $ticketsWaiting->count() . " yêu cầu nghỉ phép chưa được duyệt"; + + // Nếu muốn thêm chi tiết từng ticket waiting + if ($ticketsWaiting->count() > 0) { + $waitingTicketsMessage .= ":\n"; + foreach ($ticketsWaiting as $ticket) { + $startDateFormat = Carbon::parse($ticket->start_date)->format('d/m/Y'); + $endDateFormat = Carbon::parse($ticket->end_date)->format('d/m/Y'); + $waitingTicketsMessage .= "- " . $ticket->startPeriodName . " (" . $startDateFormat . ") - " . + $ticket->endPeriodName . " (" . $endDateFormat . ")\n"; + } + } + } + + $balanceCheckResultWaiting = $this->checkLeaveBalance($user, $dataListPeriodWaiting); + // dd($balanceCheckResultWaiting,$dataListPeriodWaiting,$user); + if ($balanceCheckResultWaiting['months_info']) { + $monthsInfoWaiting = $balanceCheckResultWaiting['months_info']; + if ($balanceCheckResultWaiting['success']) { + $waitingTicketsMessage .= "------------------------------------------------"; + } else { + $waitingTicketsMessage .= $balanceCheckResultWaiting['message'] . "\n------------------------------------------------"; + } + $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod, $monthsInfoWaiting); + // dd($balanceCheckResult, $waitingTicketsMessage); + } else { + $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod); + } + // dd($balanceCheckResult); + // Nếu không đủ ngày phép, trả về thông báo và chưa tạo ticket + if (!$balanceCheckResult['success']) { + $finalMessage = $waitingTicketsMessage; + if (!empty($finalMessage)) { + $finalMessage .= "\n\n"; + } + $finalMessage .= $balanceCheckResult['message']; + $balanceCheckResult['message'] = $finalMessage . "\n\nBạn có chấp nhận không?\n"; + $balanceCheckResult['waitingTicketMessage'] = $waitingTicketsMessage; + + return AbstractController::ResultError("Không thỏa mãn điều kiện ngày phép", $balanceCheckResult); + } + } + // --- Kết thúc kiểm tra --- + + // Tạo ticket mới khi: + // - Ticket được tạo không trùng ngày + // - User có đủ phép + // - User không đủ phép nhưng vẫn đồng ý tạo $ticket = Ticket::create([ - 'start_date' => Carbon::create($startDate)->setTimezone(env('TIME_ZONE')), + 'start_date' => $start_date->toDateString(), 'start_period' => $startPeriod, - 'end_date' => Carbon::create($endDate)->setTimezone(env('TIME_ZONE')), + 'end_date' => $end_date->toDateString(), 'end_period' => $endPeriod, 'type' => $type, 'status' => 'WAITING', @@ -235,24 +347,479 @@ class TicketController extends Controller ]); // Send notification email to admin (list) + $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod); + $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod); + $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type); + + $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y'); + $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y'); + $admins = Admin::where('permission', 'like', '%admin%')->get(); foreach ($admins as $key => $value) { $data = array( + "ticket_id" => $ticket->id, "email_template" => "email.notification_tickets", "email" => $user->email, + "admin_email" => $value->email, "name" => $user->name, - "date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")", - "type" => $dataMasterType->c_name, + "date" => optional($dataMasterStartPeriod)->c_name . " (" . $formattedStartDate . ") - " . optional($dataMasterEndPeriod)->c_name . " (" . $formattedEndDate . ")", + "type" => optional($dataMasterType)->c_name, "note" => $reason, "link" => "/tickets-management", //link đến page admin "subject" => "[Ticket request] Ticket From " . $user->name ); - Mail::to($value->email)->send(new TicketMail($data)); + // Thêm kiểm tra null trước khi gửi mail + if ($dataMasterStartPeriod && $dataMasterEndPeriod && $dataMasterType) { + Mail::to($value->email)->queue(new TicketMail($data)); + } else { + Log::error("Missing category data for ticket ID: {$ticket->id}. Mail not sent."); + } } return response()->json(['data' => $ticket, 'status' => true]); } + public function updateTicket(Request $request) + { + // Validate input + $rules = [ + 'ticket_id' => 'required|exists:tickets,id', + 'status' => 'required|string|in:CONFIRMED,REFUSED' + ]; + $request->validate($rules); + + $ticket = Ticket::find($request->input('ticket_id')); + if (!$ticket) { + return AbstractController::ResultError("Ticket not found."); + } + + $admin = auth('admins')->user(); + // Delete related note, if status change to Refuse + if ($request->status == "REFUSED") { + $ticket->status = "REFUSED"; + + // 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)); + + Notes::where('ticket_id', $ticket->id)->delete(); + } + + $ticket->updated_by = $admin->name; + $ticket->admin_note = $request->admin_note; + + // Clear Timekeeping cache + $ticket->save(); + + $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 AbstractController::ResultSuccess($ticket, "Ticket updated successfully!"); + } + + /** + * Kiểm tra số dư ngày phép của người dùng. + * + * @param UserModel $user Người dùng tạo ticket + * @param array|null $dataListPeriod Danh sách các ngày xin nghỉ [['date' => 'Y-m-d', 'period' => 'ALL|S|C'], ...] + * @return array Kết quả kiểm tra ['success' => bool, 'message' => string|null, ...] + */ + private function checkLeaveBalance($user, ?array $dataListPeriod = null, ?array $monthsInfoWaiting = null, ?bool $isAccept = false): array + { + // Kiểm tra giới hạn nghỉ phép theo tháng + if (!empty($dataListPeriod)) { + return $this->checkMonthlyLeaveLimit($user, $dataListPeriod, $monthsInfoWaiting, $isAccept); + } + + // Đủ điều kiện + return [ + 'success' => true, + 'message' => null, + 'months_info' => [] + ]; + } + + private function getTotalAllocatedDays($user, int $year): float + { + $leaveDaysInfo = LeaveDays::where('ld_user_id', $user->id) + ->where('ld_year', $year) + ->first(); + + $totalAllocated = 0; + if ($leaveDaysInfo) { + $totalAllocated = $leaveDaysInfo->ld_day_total + $leaveDaysInfo->ld_additional_day + $leaveDaysInfo->ld_special_leave_day; + } else { + Log::warning("No LeaveDays record found for user ID: {$user->id}, year: {$year}. Assuming 0 allocated days."); + } + + return $totalAllocated; + } + + private function getUsedLeaveDays($user, int $year): float + { + return Notes::join('categories', function ($join) { + $join->on('notes.n_time_type', '=', 'categories.c_code') + ->where('categories.c_type', 'TIME_TYPE'); + }) + ->where('n_user_id', $user->id) + ->where('n_year', $year) + ->where('n_reason', 'ONLEAVE') + ->sum('categories.c_value'); + } + + //Tính tổng giới hạn ngày nghỉ có phép tối đa trong tháng + private function getMaxLeaveDaysPerMonth(): int + { + $limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first(); + if ($limitLeaveMonth) { + $maxDaysPerMonth = (int)$limitLeaveMonth->c_value; + } else { + $maxDaysPerMonth = 3; // default nếu k có setting + } + return $maxDaysPerMonth; + } + + private function checkMonthlyLeaveLimit($user, array $dataListPeriod, ?array $monthsInfoWaiting = null, ?bool $isAccept = false): array + { + // Danh sách ngày nghỉ theo tháng + $requestMonths = $this->groupLeaveRequestsByMonth($dataListPeriod); + $monthsInfo = []; + $hasInsufficientDays = false; + $errorMessage = ''; + $remainingDaysInMonthIsUsed = 0; + + // Tổng giới hạn ngày nghỉ có phép tối đa trong tháng + $maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth(); + $monthIndex = 0; + $onleaveTmp = 0; // Ngày phép trừ tạm (tính phép cho nhiều tháng) + + foreach ($requestMonths as $monthData) { + if ($monthsInfoWaiting) { + foreach ($monthsInfoWaiting as $monthInfo) { + if ($monthInfo['month'] == $monthData['month'] && $monthInfo['year'] == $monthData['year']) { + $remainingDaysInMonthIsUsed += $monthInfo['remaining_days_in_month_remaining']; + } + } + } + // Số ngày nghỉ trong tháng + $onleaveDaysInMonth = $this->getUsedLeaveDaysInMonth($user, $monthData['year'], $monthData['month'], 'ONLEAVE'); // Có phép + $nopayDaysInMonth = $this->getUsedLeaveDaysInMonth($user, $monthData['year'], $monthData['month'], 'LEAVE_WITHOUT_PAY'); // Không phép + $usedDaysInMonth = $onleaveDaysInMonth + $nopayDaysInMonth; // Tổng + + // Tổng ngày nghỉ sẽ dùng trong tháng + $willUsedDaysInMonth = $usedDaysInMonth + $monthData['days_requested']; + + // Ngày phép + $onleaveDaysTotal = $this->getTotalLeaveDaysInMonth($user, $monthData['year'], $monthData['month'], $isAccept); // Tổng phép của user + $usedOnleaveDaysTotal = $this->getTotalLeaveDaysInMonthToMonth($user, $monthData['year'], $monthData['month']); // Phép đã dùng + $remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal - $onleaveTmp; // Phép còn lại + + // Tổng ngày phép còn lại trong tháng + $remainingOnleaveDaysInMonth = $remainingOnleaveDays - $remainingDaysInMonthIsUsed; + + // Log::debug( + // "📊 Thống kê ngày phép:\n" . + // " - Tháng: {$monthData['month']}\n" . + // " - Tổng ngày nghỉ có phép trong tháng: $onleaveDaysInMonth\n" . + // " - Tổng ngày nghỉ không phép trong tháng: $nopayDaysInMonth\n" . + // " - Tổng ngày nghỉ đã dùng trong tháng: $usedDaysInMonth\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" . + // " - Tổng ngày phép còn lại trong tháng: $remainingOnleaveDaysInMonth\n" + // ); + + $month_data_status = 'ok'; + $onleave_days_will_use = 0; // Ngày phép sẽ dùng trong tháng + $nopay_days_will_use = 0; // Ngày ko phép sẽ dùng trong tháng + + // Ngày phép còn lại <= 0 (Hết phép) hoặc là nhân viên chưa chính thức + if ($remainingOnleaveDaysInMonth <= 0 || !$user->is_permanent) { + $hasInsufficientDays = true; + $month_data_status = 'no_days_left'; + $onleave_days_will_use = 0; + $nopay_days_will_use = $monthData['days_requested']; + + // Message cảnh báo nghỉ ko phép + $monthMessage = $this->buildMonthlyLeaveMessage( + $monthIndex, + $maxDaysPerMonth, + $monthData, + $remainingOnleaveDaysInMonth, + $onleaveDaysInMonth, + $nopayDaysInMonth, + $onleave_days_will_use, + $nopay_days_will_use + ); + $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage; + + Log::debug("--- Hết phép trong tháng ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + + // Ngày phép còn lại < ngày yêu cầu (Không đủ phép) + else if ($remainingOnleaveDaysInMonth < $monthData['days_requested']) { + // Vượt limit + if ($willUsedDaysInMonth > $maxDaysPerMonth) { + $hasInsufficientDays = true; + $month_data_status = 'exceed_max_days'; + + // Phép còn lại > limit + if ($remainingOnleaveDaysInMonth > $maxDaysPerMonth) { + $onleave_days_will_use = $maxDaysPerMonth - $onleaveDaysInMonth; + $nopay_days_will_use = $monthData['days_requested'] - $maxDaysPerMonth + $onleaveDaysInMonth; + } + // Phép còn lại < limit + else { + $onleave_days_will_use = $remainingOnleaveDaysInMonth; + $nopay_days_will_use = $monthData['days_requested'] - $remainingOnleaveDaysInMonth; + } + + Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + // Không vượt limit + else { + $hasInsufficientDays = true; + $month_data_status = 'insufficient_days'; + $onleave_days_will_use = $remainingOnleaveDaysInMonth; + $nopay_days_will_use = $monthData['days_requested'] - $remainingOnleaveDaysInMonth; + + Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + + // Message cảnh báo nghỉ ko phép + $monthMessage = $this->buildMonthlyLeaveMessage( + $monthIndex, + $maxDaysPerMonth, + $monthData, + $remainingOnleaveDaysInMonth, + $onleaveDaysInMonth, + $nopayDaysInMonth, + $onleave_days_will_use, + $nopay_days_will_use + ); + $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage; + } + + // Ngày phép còn lại >= ngày yêu cầu (Đủ phép) + else { + // Vượt limit + if ($willUsedDaysInMonth > $maxDaysPerMonth) { + $hasInsufficientDays = true; + $month_data_status = 'exceed_max_days'; + $onleave_days_will_use = $maxDaysPerMonth - $onleaveDaysInMonth; + $nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth - $nopayDaysInMonth; + + Log::debug("--- Đủ phép, vượt limit ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + // Waiting ticket + else if ($monthData['days_requested'] + $remainingDaysInMonthIsUsed > $maxDaysPerMonth) { + if ($remainingDaysInMonthIsUsed > 0) { + $onleave_days_will_use = $maxDaysPerMonth - $remainingDaysInMonthIsUsed; + $nopay_days_will_use = $monthData['days_requested'] - $onleave_days_will_use; + } else { + $onleave_days_will_use = $maxDaysPerMonth; + $nopay_days_will_use = $monthData['days_requested'] - $maxDaysPerMonth; + } + $hasInsufficientDays = true; + $month_data_status = 'exceed_max_days'; + + Log::debug("--- Đủ phép, Waiting ticket ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + // Đủ phép + else { + $onleave_days_will_use = $monthData['days_requested']; + $nopay_days_will_use = 0; + + Log::debug("--- Đủ phép ---", [ + "Phep" => $onleave_days_will_use, + "Khong Phep" => $nopay_days_will_use + ]); + } + + // Message cảnh báo nghỉ ko phép + $monthMessage = $this->buildMonthlyLeaveMessage( + $monthIndex, + $maxDaysPerMonth, + $monthData, + $remainingOnleaveDaysInMonth, + $onleaveDaysInMonth, + $nopayDaysInMonth, + $onleave_days_will_use, + $nopay_days_will_use + ); + $errorMessage .= $errorMessage ? "\n\n" . $monthMessage : $monthMessage; + + $remainingOnleaveDaysInMonth = $monthData['days_requested']; + } + + $month_data = [ + 'year' => $monthData['year'], + 'month' => $monthData['month'], + 'total_leave_days_in_month' => $onleaveDaysTotal, // tổng số ngày phép + 'total_leave_days_in_month_to_month' => $usedOnleaveDaysTotal, // tổng ngày nghỉ có phép đã nghỉ + 'remaining_days_in_month' => $remainingOnleaveDays, // số ngày phép còn lại + 'days_used' => $onleaveDaysInMonth, // tổng số ngày nghỉ có phép đã nghỉ ở tháng hiện tại + 'days_used_without_pay' => $nopayDaysInMonth, // tổng số ngày nghỉ không phép đã nghỉ ở tháng hiện tại + 'days_requested' => $monthData['days_requested'], // số ngày yêu cầu nghỉ của tháng + 'remaining_days_in_month_remaining' => $remainingOnleaveDaysInMonth, + 'days_will_use' => $onleave_days_will_use, //Số ngày phép sẽ sử dụng + 'days_will_use_without_pay' => $nopay_days_will_use, //Số ngày không phép sẽ sử dụng + 'status' => $month_data_status, // mặc định là ok + ]; + + // Thêm thông tin tháng vào mảng kết quả + $monthsInfo[] = $month_data; + $monthIndex++; + $onleaveTmp += $onleave_days_will_use; // Cộng ngày phép dùng tạm trong tháng + } + + // Trả về kết quả tổng hợp + if ($hasInsufficientDays) { + return [ + 'success' => false, + 'message' => $errorMessage, + 'warning_type' => 'exceed_monthly_limit', + 'months_info' => $monthsInfo + ]; + } + + return [ + 'success' => true, + 'message' => "Đủ ngày phép cho yêu cầu.", + 'months_info' => $monthsInfo + ]; + } + + //Tính tổng số ngày nghỉ có phép đến tháng hiện tại + private function getTotalLeaveDaysInMonthToMonth($user, int $year, int $month): float + { + return Notes::join('categories', function ($join) { + $join->on('notes.n_time_type', '=', 'categories.c_code') + ->where('categories.c_type', 'TIME_TYPE'); + }) + ->where('n_user_id', $user->id) + ->where('n_year', $year) + // ->where('n_month', "<=", $month) + ->where('n_reason', 'ONLEAVE') + ->sum('categories.c_value'); + } + + private function getTotalLeaveDaysInMonth($user, int $year, int $month, ?bool $isAccept = false): float + { + $leaveDaysInfo = LeaveDays::where('ld_user_id', $user->id) + ->where('ld_year', $year) + ->first(); + + $totalAllocated = 0; + // Xử lý gửi ticket sau tháng hiện tại + if ($leaveDaysInfo && $user->is_permanent) { + $currentMonth = Carbon::now()->month; + $totalAllocated = $leaveDaysInfo->ld_day_total; + + // Check có phải là nhân viên chính thức trong năm nay + $isProbationInYear = false; + if ($user->permanent_date && $user->permanent_date !== '0000-00-00') { + $permenantYear = Carbon::parse($user->permanent_date)->year; + + if ($permenantYear === $year) { + $isProbationInYear = true; + } + } + + // Nhân viên mới + if ($isProbationInYear) { + $permanentMonth = Carbon::parse($user->permanent_date)->month; + if ($month > $currentMonth) { + $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 + + $totalAllocated = $month - ($permanentMonth - $permanentDefault); + } + } + // Nhân viên cũ + else { + if ($month > $currentMonth) { + $totalAllocated = $month; + } + } + } + $totalAllocated = $totalAllocated + $leaveDaysInfo->ld_additional_day + $leaveDaysInfo->ld_special_leave_day; + return $totalAllocated; + } + + private function groupLeaveRequestsByMonth(array $dataListPeriod): array + { + $requestMonths = []; + foreach ($dataListPeriod as $periodData) { + $date = Carbon::parse($periodData['date']); + $monthKey = $date->format('Y-m'); // YYYY-MM + + if (!isset($requestMonths[$monthKey])) { + $requestMonths[$monthKey] = [ + 'year' => $date->year, + 'month' => $date->month, + 'days_requested' => 0, + 'days_used' => 0 + ]; + } + + // Tính số ngày yêu cầu trong tháng + $dayValue = ($periodData['period'] === 'ALL') ? 1.0 : 0.5; + $requestMonths[$monthKey]['days_requested'] += $dayValue; + } + + return $requestMonths; + } + + private function getUsedLeaveDaysInMonth($user, int $year, int $month, string $reason): float + { + return Notes::join('categories', function ($join) { + $join->on('notes.n_time_type', '=', 'categories.c_code') + ->where('categories.c_type', 'TIME_TYPE'); + }) + ->where('n_user_id', $user->id) + ->where('n_year', $year) + ->where('n_month', $month) + ->where('n_reason', $reason) + ->sum('categories.c_value'); + } + public function deleteTicket(Request $request) { $rules = [ @@ -280,11 +847,9 @@ class TicketController extends Controller public function handleTicket(Request $request) { - $rules = [ 'ticket_id' => 'required', 'action' => 'required', - // 'admin_note' => 'required' ]; // Validate the request @@ -299,18 +864,92 @@ class TicketController extends Controller if (!$ticket || $ticket->status !== "WAITING") { return response()->json(['message' => "Ticket not found", 'status' => false]); } - $results = $this->getAllPeriod($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period); - // $admin->id != user_id of ticket ---> continue - // Confirm - // Add records to the notes table like function Timekeeping.addNoteForUser() based on the $results array + $dataMasterTypeNotes = CategoryController::getListMasterByType("REASON_NOTES"); + $onleave = null; + $leaveWithoutPay = null; + $temporaryOnleave = null; - // Update updated_by and admin_note in tickets table + if ($dataMasterTypeNotes) { + // get nghỉ phép, nghỉ không phép + $onleave = optional($dataMasterTypeNotes->where('c_code', 'ONLEAVE')->first())->c_code; + $leaveWithoutPay = optional($dataMasterTypeNotes->where('c_code', 'LEAVE_WITHOUT_PAY')->first())->c_code; + $temporaryOnleave = optional($dataMasterTypeNotes->where('c_code', 'TEMPORARY_ONLEAVE')->first())->c_code; + } - // Send notification email to users + if ($onleave == null || $leaveWithoutPay == null || $temporaryOnleave == null) { + return response()->json(['message' => "Data reason notes not found", 'status' => false]); + } - // Refuse - // Update updated_by and admin_note in tickets table + if ($action == "confirm") { + $this->handleConfirmTicket($ticket, $admin, $admin_note, $onleave, $leaveWithoutPay, $temporaryOnleave); + return response()->json(['message' => "confirmed", 'status' => true]); + } + + if ($action == "refuse") { + $this->handleRefuseTicket($ticket, $admin, $admin_note); + return response()->json(['message' => "refused", 'status' => true]); + } + + return response()->json(['message' => "failed", 'status' => false]); + } + + public function handleTicketEmail(Request $request) + { + $rules = [ + 'ticket_id' => 'required', + 'action' => 'required', + 'admin_email' => 'required' // Need Admin Email + ]; + + // Validate the request + $request->validate($rules); + + $ticket_id = $request->input('ticket_id'); + $admin_note = $request->input('admin_note'); + $admin_email = $request->input('admin_email'); + $action = $request->input('action'); // 'confirm' or 'refuse' + $admin = Admin::where('email', $admin_email)->first(); // Get admin by email not token + $ticket = Ticket::find($ticket_id); + + if (!$ticket || $ticket->status !== "WAITING") { + // No ticket found or already confirmed or refused + return redirect()->to(config('app.client_url') . '/404'); + } + + $dataMasterTypeNotes = CategoryController::getListMasterByType("REASON_NOTES"); + $onleave = null; + $leaveWithoutPay = null; + $temporaryOnleave = null; + + if ($dataMasterTypeNotes) { + // Get nghỉ phép, nghỉ không phép + $onleave = optional($dataMasterTypeNotes->where('c_code', 'ONLEAVE')->first())->c_code; + $leaveWithoutPay = optional($dataMasterTypeNotes->where('c_code', 'LEAVE_WITHOUT_PAY')->first())->c_code; + $temporaryOnleave = optional($dataMasterTypeNotes->where('c_code', 'TEMPORARY_ONLEAVE')->first())->c_code; + } + + // Không tìm được ngày phép, ko phép + if ($onleave == null || $leaveWithoutPay == null || $temporaryOnleave == null) { + return redirect()->to(config('app.client_url') . '/404'); + } + + if ($action == "confirm") { + $this->handleConfirmTicket($ticket, $admin, $admin_note, $onleave, $leaveWithoutPay, $temporaryOnleave); + return redirect()->to(config('app.client_url') . '/tickets-management'); + } + + if ($action == "refuse") { + $this->handleRefuseTicket($ticket, $admin, $admin_note); + return redirect()->to(config('app.client_url') . '/tickets-management'); + } + + // Failed + return redirect()->to(config('app.client_url') . '/tickets-management'); + } + + private function handleConfirmTicket($ticket, $admin, $admin_note, $onleave, $leaveWithoutPay, $temporaryOnleave) + { $startDate = $ticket->start_date; //Start day $startPeriod = $ticket->start_period; //The session begins $endDate = $ticket->end_date; //End date @@ -325,9 +964,143 @@ class TicketController extends Controller $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y'); $user = Admin::find($ticket->user_id); - - if ($action == "confirm") { - foreach ($results as $result) { + + if ($ticket->type == "ONLEAVE") { + $dataListPeriod = $this->getAllPeriodNew($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period); + $balanceCheckResult = $this->checkLeaveBalance($user, $dataListPeriod, null, true); + // dd($balanceCheckResult, $dataListPeriod); + $currentMonth = Carbon::now()->month; + if ($balanceCheckResult['success'] == false) { + if ($balanceCheckResult['months_info']) { + foreach ($balanceCheckResult['months_info'] as $monthInfo) { + // Lọc các ngày thuộc đúng tháng/năm này + $daysInMonth = array_filter($dataListPeriod, function ($item) use ($monthInfo) { + $date = \Carbon\Carbon::parse($item['date']); + return $date->year == $monthInfo['year'] && $date->month == $monthInfo['month']; + }); + + $daysWillUse = $monthInfo['days_will_use'] ?? 0; + $daysWillUseWithoutPay = $monthInfo['days_will_use_without_pay'] ?? 0; + // dd($daysWillUse,$daysWillUseWithoutPay,$daysInMonth); + foreach ($daysInMonth as $item) { + list($year, $month, $day) = explode('-', $item['date']); + $period = $item['period']; + $value = ($period === 'ALL') ? 1.0 : 0.5; + + if ($period === 'ALL' && $daysWillUse == 0.5) { + // Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép + // Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C) + Notes::create([ + 'n_user_id' => $ticket->user_id, + 'n_day' => $day, + 'n_month' => $month, + 'n_year' => $year, + 'n_time_type' => 'S', + 'n_reason' => $month > $currentMonth ? $temporaryOnleave : $onleave, + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id + ]); + Notes::create([ + 'n_user_id' => $ticket->user_id, + 'n_day' => $day, + 'n_month' => $month, + 'n_year' => $year, + 'n_time_type' => 'C', + 'n_reason' => $month > $currentMonth ? $temporaryOnleave : $leaveWithoutPay, + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id + ]); + $daysWillUse = 0; + $daysWillUseWithoutPay -= 0.5; + } elseif ($daysWillUse > 0) { + // Dùng ngày phép trước + $use = min($daysWillUse, $value); + Notes::create([ + 'n_user_id' => $ticket->user_id, + 'n_day' => $day, + 'n_month' => $month, + 'n_year' => $year, + 'n_time_type' => $period, + 'n_reason' => $month > $currentMonth ? $temporaryOnleave : $onleave, + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id + ]); + $daysWillUse -= $use; + } elseif ($daysWillUseWithoutPay > 0) { + // Hết phép, chuyển sang không phép + $use = min($daysWillUseWithoutPay, $value); + Notes::create([ + 'n_user_id' => $ticket->user_id, + 'n_day' => $day, + 'n_month' => $month, + 'n_year' => $year, + 'n_time_type' => $period, + 'n_reason' => $month > $currentMonth ? $temporaryOnleave : $leaveWithoutPay, + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id + ]); + $daysWillUseWithoutPay -= $use; + } + // Nếu cả hai đều hết thì thôi, không tạo nữa + } + } + } + } else { + //Đủ phép + foreach ($dataListPeriod as $result) { + list($year, $month, $day) = explode('-', $result['date']); + Notes::create([ + 'n_user_id' => $ticket->user_id, + 'n_day' => $day, + 'n_month' => $month, + 'n_year' => $year, + 'n_time_type' => $result['period'], + 'n_reason' => $month > $currentMonth ? $temporaryOnleave : $onleave, + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id + ]); + } + } + + // $yearCheck = Carbon::parse($endDate)->year; + // // Check giá trị ld_day_total của bảng leave_days thuộc user id đó với giá trị của list item note trong bảng notes của user id đó + // $leaveDaysInfo = LeaveDays::where('ld_user_id', $ticket->user_id) + // ->where('ld_year', $yearCheck) + // ->first(); + // if ($leaveDaysInfo) { + // // Tính tổng số ngày nghỉ có phép đã sử dụng trong năm + // $totalUsedLeaveDays = Notes::join('categories', function ($join) { + // $join->on('notes.n_time_type', '=', 'categories.c_code') + // ->where('categories.c_type', 'TIME_TYPE'); + // }) + // ->where('n_user_id', $ticket->user_id) + // ->where('n_year', $yearCheck) + // ->where('n_reason', 'ONLEAVE') + // ->sum('categories.c_value'); + + // // Tính tổng số ngày phép được cấp + // $totalAllocatedDays = $leaveDaysInfo->ld_day_total + + // $leaveDaysInfo->ld_additional_day + + // $leaveDaysInfo->ld_special_leave_day; + + // // Tính số ngày vượt quá và làm tròn lên + // $excessDays = $totalUsedLeaveDays - $totalAllocatedDays; + // $roundedExcessDays = ceil($excessDays); // Làm tròn lên số nguyên gần nhất + + // // Kiểm tra nếu số ngày đã sử dụng vượt quá số ngày được cấp + // if ($roundedExcessDays > 0) { + // Log::warning("User ID: {$ticket->user_id} has used more leave days ({$totalUsedLeaveDays}) than allocated ({$totalAllocatedDays})"); + + // // Cập nhật cột ld_day_total với số ngày đã làm tròn + // $leaveDaysInfo->ld_day_total += $roundedExcessDays; + // $leaveDaysInfo->save(); + + // Log::info("Updated leave days for User ID: {$ticket->user_id}. Added {$roundedExcessDays} days (rounded from {$excessDays})"); + // } + // } + } else if ($ticket->type == "WFH") { + $dataListPeriod = $this->getAllPeriod($ticket->start_date, $ticket->start_period, $ticket->end_date, $ticket->end_period); + foreach ($dataListPeriod as $result) { list($year, $month, $day) = explode('-', $result['date']); Notes::create([ 'n_user_id' => $ticket->user_id, @@ -336,92 +1109,212 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => $result['period'], 'n_reason' => $ticket->type, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); - if ($ticket->type == "WFH") { - $type = $result['period']; - $date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE')); - //Default: ALL - $start = $date->copy()->setTime(7, 30, 0); - $end = $date->copy()->setTime(17, 0, 0); + //WFH - start tracking + $type = $result['period']; + $date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE')); - if ($type == 'S') { - $end = $date->copy()->setTime(11, 30, 0); - } else if ($type == 'C') { - $start = $date->copy()->setTime(11, 30, 0); + //Default: ALL + $start = $date->copy()->setTime(7, 30, 0); + $end = $date->copy()->setTime(17, 0, 0); + + if ($type == 'S') { + $end = $date->copy()->setTime(11, 30, 0); + } else if ($type == 'C') { + $start = $date->copy()->setTime(11, 30, 0); + } + + Tracking::insert([ + [ + 'name' => $user->name, + 'user_id' => $user->id, + 'status' => 'check in', + 'time_string' => $start->format('Y-m-d H:i:s'), + 'created_at' => $start->setTimezone('UTC') + ], + [ + 'name' => $user->name, + 'user_id' => $user->id, + 'status' => 'check out', + 'time_string' => $end->format('Y-m-d H:i:s'), + 'created_at' => $end->setTimezone('UTC') + ] + ]); + //WFH - end tracking + } + } + + $ticket['updated_by'] = $admin->name; + $ticket['admin_note'] = $admin_note; + $ticket['status'] = 'CONFIRMED'; + $ticket->save(); + + $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); + + // Send notification email to users + $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" => $admin_note, + "link" => "/tickets", //link đến page admin + "status" => "confirmed", + "subject" => "[Ticket response] Ticket From " . $admin->name + ); + Mail::to($user->email)->send(new TicketMail($data)); + } + + private function handleRefuseTicket($ticket, $admin, $admin_note) + { + $startDate = $ticket->start_date; //Start day + $startPeriod = $ticket->start_period; //The session begins + $endDate = $ticket->end_date; //End date + $endPeriod = $ticket->end_period; //Session ends + $type = $ticket->type; + + $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod); + $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod); + $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type); + + $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y'); + $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y'); + + $user = Admin::find($ticket->user_id); + + $ticket['updated_by'] = $admin->name; + $ticket['admin_note'] = $admin_note; + $ticket['status'] = 'REFUSED'; + $ticket->save(); + + $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" => $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)); + } + + private function getAllPeriodNew($startDate, $startPeriod, $endDate, $endPeriod) + { + // Đảm bảo $startDate và $endDate là đối tượng Carbon + if (!($startDate instanceof Carbon)) { + $startDate = Carbon::parse($startDate); + } + + if (!($endDate instanceof Carbon)) { + $endDate = Carbon::parse($endDate); + } + + // Create an array to contain the results + $results = []; + + // Use CarbonPeriod to create a period from the start date to the end date + $period = CarbonPeriod::create($startDate, $endDate); + + $time_type = Category::where('c_type', 'TIME_TYPE')->get()->keyBy('c_code'); + $morning = $time_type->get('S'); + $afternoon = $time_type->get('C'); + $all_day = $time_type->get('ALL'); + + // Get all Saturday work schedules and sort them by date in descending order + $saturday_work_schedules = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE') + ->get() + ->sortByDesc(function ($item) { + // Parse the date string from c_code to a Carbon instance for proper comparison + return Carbon::createFromFormat('d-m-Y', $item->c_code); + }); + // get day work special + $day_work_special = Category::where('c_type', 'DAY_WORK_SPECIAL') + ->get() + ->sortByDesc(function ($item) { + return Carbon::createFromFormat('d-m-Y', $item->c_code); + }); + + + // Get the most recent schedule date (first item after sorting) + $latest_schedule = $saturday_work_schedules->first(); + $latestScheduleDate = Carbon::createFromFormat('d-m-Y', $latest_schedule->c_code); + + if (!$morning || !$afternoon || !$all_day) { + // Handle error: TIME_TYPE categories not found + Log::error("TIME_TYPE categories (S, C, ALL) not found in database."); + return []; // Return empty or throw exception + } + + $special_dates = []; + foreach ($day_work_special as $item) { + $special_dates[] = Carbon::createFromFormat('d-m-Y', $item->c_code)->toDateString(); + } + foreach ($period as $date) { + // Check phải ngày thứ 7 đặc biệt thì tính như ngày bình thường + if (in_array($date->toDateString(), $special_dates)) { + } else { + // Check if the current day is a Saturday + if ($date->dayOfWeek === Carbon::SATURDAY) { + if ($latest_schedule) { + $weeksDifference = $latestScheduleDate->startOfDay()->diffInWeeks($date->copy()->startOfDay()); + $isSaturdayWorkDay = ($weeksDifference % 2 === 0); + + // echo $date->toDateString() . ' - ' . ($isSaturdayWorkDay ? 'Làm việc' : 'Nghỉ') . "
"; } - Tracking::insert([ - [ - 'name' => $user->name, - 'user_id' => $user->id, - 'status' => 'check in', - 'time_string' => $start->format('Y-m-d H:i:s'), - 'created_at' => $start->setTimezone('UTC') - ], - [ - 'name' => $user->name, - 'user_id' => $user->id, - 'status' => 'check out', - 'time_string' => $end->format('Y-m-d H:i:s'), - 'created_at' => $end->setTimezone('UTC') - ] - ]); + if ($isSaturdayWorkDay) { + $results[] = ['date' => $date->toDateString(), 'period' => "S"]; + } + + continue; + } + // Skip Sundays entirely + else if ($date->dayOfWeek === Carbon::SUNDAY) { + continue; } } - $ticket['updated_by'] = $admin->name; - $ticket['admin_note'] = $admin_note; - $ticket['status'] = 'CONFIRMED'; - $ticket->save(); - - $this->createOrUpdateRecordForCurrentMonth($month, $year); - - // Send notification email to users - $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" => $admin_note, - "link" => "/tickets", //link đến page admin - "status" => "confirmed", - "subject" => "[Ticket response] Ticket From " . $admin->name - ); - Mail::to($user->email)->send(new TicketMail($data)); - - return response()->json(['message' => "confirmed", 'status' => true]); + if ($date->isSameDay($startDate)) { + //If the start date is morning, add afternoon + if ($startDate->isSameDay($endDate)) { // Nghỉ trong cùng 1 ngày + if ($startPeriod == $endPeriod) { // Cùng 1 buổi (S hoặc C) + $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod]; + } else { // Khác buổi (S đến C) -> cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } + } else { // Ngày bắt đầu khác ngày kết thúc + if ($startPeriod == $morning->c_code) { // Bắt đầu từ sáng -> tính cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } else { // Bắt đầu từ chiều -> tính buổi chiều + $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod]; // Là $afternoon->c_code + } + } + } elseif ($date->isSameDay($endDate)) { // Ngày kết thúc (khác ngày bắt đầu) + if ($endPeriod == $afternoon->c_code) { // Kết thúc vào buổi chiều -> tính cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } else { // Kết thúc vào buổi sáng -> tính buổi sáng + $results[] = ['date' => $date->toDateString(), 'period' => $endPeriod]; // Là $morning->c_code + } + } else { // Những ngày ở giữa + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } } - if ($action == "refuse") { - $ticket['updated_by'] = $admin->name; - $ticket['admin_note'] = $admin_note; - $ticket['status'] = 'REFUSED'; - $ticket->save(); - - $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" => $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)); - return response()->json(['message' => "refused", 'status' => true]); - } - - return response()->json(['message' => "failed", 'status' => false]); + // Returns results + return $results; } private function getAllPeriod($startDate, $startPeriod, $endDate, $endPeriod) @@ -488,4 +1381,221 @@ class TicketController extends Controller //Returns results return $results; } + + private function buildMonthlyLeaveMessage( + $index, + $max, + $monthData, + $totalLeave, + $usedLeave, + $usedNoPay, + $willUseLeave, + $willUseNoPay + ): string { + $message = ""; + + if ($index === 0) { + $showMonth = $monthData['month'] > Carbon::now()->month ? $monthData['month'] : Carbon::now()->month; + $message .= "* Quy định: mỗi tháng được nghỉ tối đa {$max} ngày phép\n"; + $message .= "- Bạn có: {$totalLeave} ngày phép (Tính tới {$showMonth}/{$monthData['year']})\n\n"; + } + + // Hiển thị cộng phép nếu gửi ticket trong tương lai + $monthValue = $monthData['year'] . '-' . sprintf('%02d', $monthData['month']); + $currentMonth = date('Y-m'); + if ($monthValue > $currentMonth && $index !== 0) { + $message .= "* Bạn được cộng 1 phép\n"; + } + + // In mỗi tháng + $message .= "Tháng {$monthData['month']}/{$monthData['year']}:\n"; + if ($usedLeave > 0 || $usedNoPay > 0) { + $message .= " - Bạn đã sử dụng: "; + $usedParts = []; + if ($usedLeave > 0) $usedParts[] = "{$usedLeave} phép"; + if ($usedNoPay > 0) $usedParts[] = "{$usedNoPay} không phép"; + $message .= implode(', ', $usedParts) . "\n"; + } + + if ($willUseLeave > 0 || $willUseNoPay > 0) { + $message .= " - Dự kiến bạn sẽ sử dụng: "; + $usedParts = []; + if ($willUseLeave > 0) $usedParts[] = "{$willUseLeave} phép"; + if ($willUseNoPay > 0) $usedParts[] = "{$willUseNoPay} không phép"; + $message .= implode(', ', $usedParts); + } + + return $message; + } + + /** + * Tính tổng số ngày nghỉ từ mảng các khoảng thời gian. + * 'ALL' = 1 ngày, 'S'/'C' = 0.5 ngày. + * + * @param array $dataListPeriod Mảng các khoảng thời gian nghỉ [['date' => 'Y-m-d', 'period' => 'ALL|S|C'], ...] + * @return float Tổng số ngày nghỉ + */ + private function calculateTotalLeaveDays(array $dataListPeriod): float + { + $totalDays = 0.0; + + foreach ($dataListPeriod as $periodData) { + if (isset($periodData['period'])) { + switch ($periodData['period']) { + case 'ALL': + $totalDays += 1.0; + break; + case 'S': // Buổi sáng + case 'C': // Buổi chiều + $totalDays += 0.5; + break; + // Có thể thêm default case để xử lý lỗi nếu cần + } + } + } + + return $totalDays; + } + + public function updateOldData(int $month, int $year) + { + LeaveDays::where('ld_year', $year) + ->update(['ld_day_total' => $month]); + + $users = Admin::all(); + + foreach ($users as $user) { + $leaveDay = LeaveDays::where('ld_user_id', $user->id) + ->where('ld_year', $year) + ->first(); + $notes = Notes::where('n_user_id', $user->id) + ->where('n_year', $year) + ->where('n_reason', 'ONLEAVE') + ->orderBy('n_month') + ->orderBy('n_day') + ->get() + ->groupBy('n_month'); + + $onleaveDaysTotal = $leaveDay->ld_additional_day; + + $previousYearData = LeaveDays::where('ld_user_id', $user->id) + ->where('ld_year', $year - 1) + ->first(); + + $ld_additional_day = 0; + $ld_note = ''; + + if ($previousYearData) { + $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'); + }) + ->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', $year - 1) + ->where('notes.n_user_id', $user->id) + ->where('notes.n_reason', 'ONLEAVE') + ->groupBy(DB::raw('notes.n_year')) + ->first(); + if ($totalLeaveDaysByMonth) { + $ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days; + if ($ld_additional_day < 0) { + $ld_additional_day = 0; + } + } + + if ($ld_additional_day > 0) { + $ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n"; + } + } + + for ($i = 1; $i <= $month; $i++) { + // Giả lập cộng phép + $onleaveDaysTotal++; + // $tmpOnleaveDaysTotal = $onleaveDaysTotal; + + $onleaveDaysInMonth = 0; + $nopayDaysInMonth = 0; + + if ($notes->has($i)) { + foreach ($notes[$i] as $note) { + $onleaveDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5; + } + + if ($onleaveDaysInMonth > $onleaveDaysTotal) { + $nopayDaysInMonth = $onleaveDaysInMonth - $onleaveDaysTotal; + $onleaveDaysTotal = 0; + } else { + $onleaveDaysTotal -= $onleaveDaysInMonth; + } + + // Xử lý cập nhật lại các note có phép thành không phép + if ($nopayDaysInMonth > 0) { + $revertNotes = $notes->get($i, collect())->reverse(); + $nopayDaysUpdated = 0; + + foreach ($revertNotes as $note) { + if ($note->n_time_type == 'ALL') { + if ($nopayDaysInMonth - $nopayDaysUpdated == 0.5) { + 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(); + break; + } + + $nopayDaysUpdated += 1.0; + $note->update([ + 'n_reason' => "LEAVE_WITHOUT_PAY" + ]); + } else { + $nopayDaysUpdated += 0.5; + $note->update([ + 'n_reason' => "LEAVE_WITHOUT_PAY" + ]); + } + + if ($nopayDaysUpdated >= $nopayDaysInMonth) { + break; + } + } + } + } + + // Log thông kê sau mỗi tháng + // Log::debug( + // "📊 Thống kê ngày phép Tháng {$i}:\n" . + // " - Tổng phép đầu tháng: $tmpOnleaveDaysTotal\n" . + // " - Có phép: $onleaveDaysInMonth\n" . + // " - Không phép: $nopayDaysInMonth\n" . + // " - Tổng phép cuối tháng: $onleaveDaysTotal\n" + // ); + } + + $leaveDay->ld_note = $ld_note; + $leaveDay->save(); + } + } } diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php index cddc744..f0a046c 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php @@ -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( diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index 8f9ee2c..fe3832e 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -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'); diff --git a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php index bb1e898..ff345da 100755 --- a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php +++ b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php @@ -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) diff --git a/BACKEND/Modules/Auth/app/Models/User.php b/BACKEND/Modules/Auth/app/Models/User.php index 0fc45ba..4347eca 100755 --- a/BACKEND/Modules/Auth/app/Models/User.php +++ b/BACKEND/Modules/Auth/app/Models/User.php @@ -25,7 +25,9 @@ class User extends Authenticatable implements JWTSubject 'name', 'email', 'password', - 'permission' + 'permission', + 'is_permanent', + 'permanent_date' ]; /** diff --git a/BACKEND/app/Console/Commands/AddMonthlyLeaveDaysCommand.php b/BACKEND/app/Console/Commands/AddMonthlyLeaveDaysCommand.php new file mode 100644 index 0000000..267c767 --- /dev/null +++ b/BACKEND/app/Console/Commands/AddMonthlyLeaveDaysCommand.php @@ -0,0 +1,24 @@ +argument('month'); + $year = $this->argument('year'); + AddMonthlyLeaveDays::dispatch($month, $year); + } +} diff --git a/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php b/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php index ad0a9e9..bf74a72 100644 --- a/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php +++ b/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php @@ -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); } } diff --git a/BACKEND/app/Console/Commands/UpdateTemporaryLeaveDaysCommand.php b/BACKEND/app/Console/Commands/UpdateTemporaryLeaveDaysCommand.php new file mode 100644 index 0000000..9cb2db8 --- /dev/null +++ b/BACKEND/app/Console/Commands/UpdateTemporaryLeaveDaysCommand.php @@ -0,0 +1,24 @@ +argument('month'); + $year = $this->argument('year'); + UpdateTemporaryLeaveDays::dispatch($month, $year); + } +} diff --git a/BACKEND/app/Console/Kernel.php b/BACKEND/app/Console/Kernel.php index 6d56e12..3e9cf06 100755 --- a/BACKEND/app/Console/Kernel.php +++ b/BACKEND/app/Console/Kernel.php @@ -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'); } /** diff --git a/BACKEND/app/Exports/LeaveManagementExport.php b/BACKEND/app/Exports/LeaveManagementExport.php index 038ade9..402353b 100644 --- a/BACKEND/app/Exports/LeaveManagementExport.php +++ b/BACKEND/app/Exports/LeaveManagementExport.php @@ -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); diff --git a/BACKEND/app/Jobs/AddMonthlyLeaveDays.php b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php new file mode 100644 index 0000000..15372f4 --- /dev/null +++ b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php @@ -0,0 +1,82 @@ +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(); + } + } + } + } +} diff --git a/BACKEND/app/Jobs/DeductLeaveDays.php b/BACKEND/app/Jobs/DeductLeaveDays.php index 86dc3b0..3dd165a 100644 --- a/BACKEND/app/Jobs/DeductLeaveDays.php +++ b/BACKEND/app/Jobs/DeductLeaveDays.php @@ -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(); } } } diff --git a/BACKEND/app/Jobs/InitializeLeaveDays.php b/BACKEND/app/Jobs/InitializeLeaveDays.php index 3c545b5..424745c 100644 --- a/BACKEND/app/Jobs/InitializeLeaveDays.php +++ b/BACKEND/app/Jobs/InitializeLeaveDays.php @@ -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(), diff --git a/BACKEND/app/Jobs/UpdateTemporaryLeaveDays.php b/BACKEND/app/Jobs/UpdateTemporaryLeaveDays.php new file mode 100644 index 0000000..f578d28 --- /dev/null +++ b/BACKEND/app/Jobs/UpdateTemporaryLeaveDays.php @@ -0,0 +1,220 @@ +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; + } +} diff --git a/BACKEND/app/Models/LeaveDays.php b/BACKEND/app/Models/LeaveDays.php index 67b2024..a974950 100644 --- a/BACKEND/app/Models/LeaveDays.php +++ b/BACKEND/app/Models/LeaveDays.php @@ -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'; diff --git a/BACKEND/app/Models/Notes.php b/BACKEND/app/Models/Notes.php index 57e4e49..f1a90a7 100644 --- a/BACKEND/app/Models/Notes.php +++ b/BACKEND/app/Models/Notes.php @@ -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(); + } } diff --git a/BACKEND/config/app.php b/BACKEND/config/app.php index 6e6a756..8b3161b 100755 --- a/BACKEND/config/app.php +++ b/BACKEND/config/app.php @@ -59,6 +59,8 @@ return [ 'asset_url' => env('ASSET_URL'), + 'client_url' => env('ADMIN_URL', 'http://localhost'), + /* |-------------------------------------------------------------------------- | Application Timezone diff --git a/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php new file mode 100644 index 0000000..e280937 --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php @@ -0,0 +1,22 @@ +renameColumn('ld_day', 'ld_day_total'); + }); + } + + public function down() + { + Schema::table('leave_days', function (Blueprint $table) { + $table->renameColumn('ld_day_total', 'ld_day'); + }); + } +} diff --git a/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php new file mode 100644 index 0000000..863d79f --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php @@ -0,0 +1,22 @@ +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 + }); + } +} diff --git a/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php new file mode 100644 index 0000000..b6be1ae --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php @@ -0,0 +1,22 @@ +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'); + }); + } +} \ No newline at end of file diff --git a/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php b/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php new file mode 100644 index 0000000..9cd370f --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php @@ -0,0 +1,41 @@ +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']); + } +}; diff --git a/BACKEND/database/migrations/2025_05_07_023335_update_leave_categories.php b/BACKEND/database/migrations/2025_05_07_023335_update_leave_categories.php new file mode 100644 index 0000000..ddb1b92 --- /dev/null +++ b/BACKEND/database/migrations/2025_05_07_023335_update_leave_categories.php @@ -0,0 +1,52 @@ +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']); + } +} diff --git a/BACKEND/database/migrations/2025_05_07_024806_add_limit_leave_month_category.php b/BACKEND/database/migrations/2025_05_07_024806_add_limit_leave_month_category.php new file mode 100644 index 0000000..91cb49f --- /dev/null +++ b/BACKEND/database/migrations/2025_05_07_024806_add_limit_leave_month_category.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/BACKEND/database/migrations/2025_05_07_024949_add_saturday_work_schedule_category.php b/BACKEND/database/migrations/2025_05_07_024949_add_saturday_work_schedule_category.php new file mode 100644 index 0000000..c0e51c8 --- /dev/null +++ b/BACKEND/database/migrations/2025_05_07_024949_add_saturday_work_schedule_category.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/BACKEND/database/migrations/2025_05_07_062606_add_day_work_special_category.php b/BACKEND/database/migrations/2025_05_07_062606_add_day_work_special_category.php new file mode 100644 index 0000000..c73131d --- /dev/null +++ b/BACKEND/database/migrations/2025_05_07_062606_add_day_work_special_category.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/BACKEND/database/migrations/2025_05_07_092903_add_leave_categories.php b/BACKEND/database/migrations/2025_05_07_092903_add_leave_categories.php new file mode 100644 index 0000000..30ffb7f --- /dev/null +++ b/BACKEND/database/migrations/2025_05_07_092903_add_leave_categories.php @@ -0,0 +1,60 @@ +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(); + } +} diff --git a/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php b/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php new file mode 100644 index 0000000..1a9b528 --- /dev/null +++ b/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php @@ -0,0 +1,26 @@ +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'); + }); + } +}; diff --git a/BACKEND/database/migrations/2025_06_23_098764_add_permanent_users_table.php b/BACKEND/database/migrations/2025_06_23_098764_add_permanent_users_table.php new file mode 100644 index 0000000..d761819 --- /dev/null +++ b/BACKEND/database/migrations/2025_06_23_098764_add_permanent_users_table.php @@ -0,0 +1,31 @@ +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'); + }); + } +}; diff --git a/BACKEND/database/migrations/2025_06_30_025714_add_onleave_permanent_temporary_categories_table.php b/BACKEND/database/migrations/2025_06_30_025714_add_onleave_permanent_temporary_categories_table.php new file mode 100644 index 0000000..968b07d --- /dev/null +++ b/BACKEND/database/migrations/2025_06_30_025714_add_onleave_permanent_temporary_categories_table.php @@ -0,0 +1,45 @@ +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(); + } +}; diff --git a/BACKEND/resources/views/email/notification_tickets.blade.php b/BACKEND/resources/views/email/notification_tickets.blade.php index a013eed..f9c4d13 100644 --- a/BACKEND/resources/views/email/notification_tickets.blade.php +++ b/BACKEND/resources/views/email/notification_tickets.blade.php @@ -1,4 +1,3 @@ - @@ -37,119 +36,167 @@ {{ $data['subject'] }} - - - - - - + +
- + + - - - + + + - - - + + + - -
+ - - - -
- - Logo - -
-
- + + Logo + + + +
+
+ - - - - - - - - - - - - - - - -
-

- Dear Admin, -

-
-

- Employee {{ $data['name'] }} has sent a request ticket, the specific content is as follows: -

-
-
-

Name: {{ $data['name'] }}

-

Date: {{ $data['date'] }}

-

Type: {{ $data['type'] }}

-

Note: {{ $data['note'] }}

-
-
-

- - Check now -

-
-
- + Dear Admin, + + + + + + + + + + + + + + + + + + + +
+

+ Employee {{ $data['name'] }} has sent a request ticket, the specific content is as follows: +

+
+
+

Name: {{ $data['name'] }}

+

Date: {{ $data['date'] }}

+

Type: {{ $data['type'] }}

+

Note: {{ $data['note'] }}

+
+
+

+ You can quick + Confirm or + Refuse here: +

+ + +
+
+

Note: If you are redirected to a 404 page, it means:

+

1. The ticket has already been approved by another admin.

+

2. The ticket has been deleted.

+
+
+
+ - - - -
-
-

© 2024 APAC Tech.

-
-
-
- +

© 2024 APAC Tech.

+ +
+ + + - + \ No newline at end of file diff --git a/BACKEND/tests/UpdateOldNoteData.php b/BACKEND/tests/UpdateOldNoteData.php new file mode 100644 index 0000000..3c0a458 --- /dev/null +++ b/BACKEND/tests/UpdateOldNoteData.php @@ -0,0 +1,13 @@ +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 diff --git a/FRONTEND/public/404Image.jpg b/FRONTEND/public/404Image.jpg new file mode 100644 index 0000000..76cc48b Binary files /dev/null and b/FRONTEND/public/404Image.jpg differ diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index b1c613f..54a03d7 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -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 diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 1a8b020..86b543e 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -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([]) const [date, setDate] = useState({ year: new Date().getFullYear().toString(), }) + // const [dataTimeType, setDataTimeType] = useState([]) + // const [dataReason, setDataReason] = useState([]) + + 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 ( -
- {isNewMonth &&

Month {lastmonth}

} -

- - {itemDay.reason_name} ({itemDay.time_type_name}) {itemDay.day} - /{itemDay.month} -

-
+ + + {totalOnLeaveMonth > 0 && ( + + {totalOnLeaveMonth} có phép + + )} + {totalLeaveWithoutPayMonth > 0 && ( + + {totalLeaveWithoutPayMonth} không phép + + )} + + ({d.value}/2025) + + + ) }) } + 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 ( + + + {label} + + + {value} + + + ) + } + + return ( + + + {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')} + + + ) + } + 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 (
-

- Leave Management -

+

Leave Management

+ {/* Update Leave Day */} { }) } }} - label={'Total Leave'} - placeholder="Input placeholder" + label={'Phép năm'} + placeholder="Nhập số ngày phép năm" /> { }) } }} - label={'Day additional leave'} - placeholder="Input placeholder" + label={'Phép năm cũ'} + placeholder="Nhập số ngày phép năm cũ" + /> + { + 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" /> - +