diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index 548446a..e3eaf52 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -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']) // ->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, diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php index 1e3e27b..ad00239 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php @@ -20,6 +20,10 @@ 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 Illuminate\Http\JsonResponse; +use App\Models\Admin as UserModel; class TicketController extends Controller { @@ -204,29 +208,47 @@ class TicketController extends Controller $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 data from request + $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'); + $isAccept = $request->input('is_accept') ?? false; $user = auth('admins')->user(); // user create ticket - // return $user; + $start_date = Carbon::create($startDate)->setTimezone(env('TIME_ZONE')); + $end_date = Carbon::create($endDate)->setTimezone(env('TIME_ZONE')); + $currentYear = $start_date->year; - $dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $startPeriod); - $dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $endPeriod); - $dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type); + // --- Chỉ kiểm tra ngày phép khi loại là ONLEAVE --- + if ($type === 'ONLEAVE' && !$isAccept) { + // Get mảng ngày nghỉ và tính tổng số ngày yêu cầu + $dataListPeriod = $this->getAllPeriodNew($start_date, $startPeriod, $end_date, $endPeriod); + if (empty($dataListPeriod)) { + return response()->json(['message' => 'Không thể tính toán khoảng thời gian nghỉ hợp lệ.', 'status' => false]); + } - $formattedStartDate = Carbon::createFromFormat('Y-m-d', $startDate)->format('d/m/Y'); - $formattedEndDate = Carbon::createFromFormat('Y-m-d', $endDate)->format('d/m/Y'); - - $user = auth('admins')->user(); + // Kiểm tra số dư ngày phép + $balanceCheckResult = $this->checkLeaveBalance($user, $currentYear, $dataListPeriod); + dd($balanceCheckResult); + // Nếu không đủ ngày phép, trả về thông báo và không tạo ticket + if (!$balanceCheckResult['success']) { + return response()->json([ + 'message' => $balanceCheckResult['message'], + 'status' => false + ]); + } + } + dd("đã qua kiểm tra"); + // --- Kết thúc kiểm tra --- + + // Nếu đủ ngày phép (hoặc loại ticket không phải ONLEAVE), tiếp tục tạo ticket $ticket = Ticket::create([ - '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 +257,237 @@ 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( "email_template" => "email.notification_tickets", "email" => $user->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)->send(new TicketMail($data)); + } else { + Log::error("Missing category data for ticket ID: {$ticket->id}. Mail not sent."); + } } return response()->json(['data' => $ticket, 'status' => true]); } + /** + * 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 int $year Năm kiểm tra + * @param float $daysRequested Số ngày yêu cầu nghỉ + * @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, int $year, array $dataListPeriod = null): array + { + // Tính tổng số ngày yêu cầu + $daysRequested = $this->calculateTotalLeaveDays($dataListPeriod); + + // 1. Tính tổng ngày phép được cấp + $totalAllocated = $this->getTotalAllocatedDays($user, $year); + + // 2. Tính số ngày đã nghỉ trong năm + $usedDays = $this->getUsedLeaveDays($user, $year); + + // 3. Tính số ngày còn lại + $remainingDays = $totalAllocated - $usedDays; + + // 4. Kiểm tra giới hạn nghỉ phép theo tháng + $monthsInfo = []; + if (!empty($dataListPeriod)) { + $monthlyCheckResult = $this->checkMonthlyLeaveLimit($user, $dataListPeriod); + if (!$monthlyCheckResult['success']) { + return $monthlyCheckResult; + } + $monthsInfo = $monthlyCheckResult['months_info']; + if (!empty($monthsInfo)) { + //Danh sách ngày nghỉ trong tháng dựa trên list ngày xin nghỉ + + } + } + + // 5. Kiểm tra đủ ngày phép không + if ($remainingDays < $daysRequested) { + return $this->insufficientLeaveDaysResponse($user, $remainingDays, $daysRequested); + } + + // 6. Kiểm tra giới hạn ngày liên tục + if ($daysRequested > 3) { + return $this->exceedMaxConsecutiveDaysResponse($user, $daysRequested); + } + + // Đủ điều kiện + return [ + 'success' => true, + 'message' => null, + 'remaining_days' => $remainingDays, + 'months_info' => $monthsInfo + ]; + } + + 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'); + } + + private function checkMonthlyLeaveLimit($user, array $dataListPeriod): array + { + $requestMonths = $this->groupLeaveRequestsByMonth($dataListPeriod); + $monthsInfo = []; + + foreach ($requestMonths as $monthKey => $monthData) { + $usedDaysInMonth = $this->getUsedLeaveDaysInMonth($user, $monthData['year'], $monthData['month']); + dd($usedDaysInMonth); + $requestMonths[$monthKey]['days_used'] = $usedDaysInMonth; + + // Kiểm tra giới hạn ngày nghỉ trong tháng + $maxDaysPerMonth = 3; // Có thể điều chỉnh theo quy định công ty + $totalDaysInMonth = $usedDaysInMonth + $monthData['days_requested']; + + if ($totalDaysInMonth > $maxDaysPerMonth) { + return $this->exceedMonthlyLimitResponse($monthData, $usedDaysInMonth, $maxDaysPerMonth); + } + + $monthsInfo[] = [ + 'year' => $monthData['year'], + 'month' => $monthData['month'], + 'days_used' => $usedDaysInMonth, + 'days_requested' => $monthData['days_requested'], + 'total_days' => $totalDaysInMonth + ]; + } + + return [ + 'success' => true, + 'months_info' => $monthsInfo + ]; + } + + 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): 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 exceedMonthlyLimitResponse(array $monthData, float $usedDaysInMonth, int $maxDaysPerMonth): array + { + $exceedDays = ($usedDaysInMonth + $monthData['days_requested']) - $maxDaysPerMonth; + + return [ + 'success' => false, + 'message' => "Trong tháng {$monthData['month']}/{$monthData['year']}, bạn đã nghỉ {$usedDaysInMonth} ngày và xin nghỉ thêm {$monthData['days_requested']} ngày, vượt quá giới hạn {$maxDaysPerMonth} ngày/tháng. Vui lòng điều chỉnh lại.", + 'warning_type' => 'exceed_monthly_limit', + 'month_data' => [ + 'year' => $monthData['year'], + 'month' => $monthData['month'], + 'days_used' => $usedDaysInMonth, + 'days_requested' => $monthData['days_requested'], + 'max_days' => $maxDaysPerMonth, + 'exceed_days' => $exceedDays + ] + ]; + } + + private function insufficientLeaveDaysResponse($user, float $remainingDays, float $daysRequested): array + { + $daysNotEnough = $daysRequested - $remainingDays; + Log::warning("Insufficient leave balance for user ID: {$user->id}. Remaining: {$remainingDays}, Requested: {$daysRequested}"); + + return [ + 'success' => false, + 'message' => "Bạn không đủ ngày phép. Số ngày phép còn lại: {$remainingDays}, Số ngày yêu cầu: {$daysRequested}. Bạn có chấp nhận nộp: {$daysNotEnough} ngày không phép không?" + ]; + } + + private function exceedMaxConsecutiveDaysResponse($user, float $daysRequested): array + { + $noLeavePermissionDays = $daysRequested - 3; + $message = "Bạn đã yêu cầu {$daysRequested} ngày nghỉ. Theo quy định, chỉ 3 ngày đầu là nghỉ có phép, {$noLeavePermissionDays} ngày còn lại sẽ là nghỉ không phép. Bạn có chấp nhận tiếp tục không?"; + Log::info("User ID: {$user->id} requested {$daysRequested} days which exceeds the 3-day limit. {$noLeavePermissionDays} days will be marked as leave without permission."); + + return [ + 'success' => true, + 'message' => $message, + 'require_acceptance' => true, + 'warning_type' => 'exceed_max_days', + 'max_leave_days' => 3, + 'days_with_permission' => 3, + 'days_without_permission' => $noLeavePermissionDays + ]; + } + public function deleteTicket(Request $request) { $rules = [ @@ -325,7 +560,7 @@ 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) { list($year, $month, $day) = explode('-', $result['date']); @@ -424,6 +659,70 @@ class TicketController extends Controller return response()->json(['message' => "failed", 'status' => false]); } + private function getAllPeriodNew($startDate, $startPeriod, $endDate, $endPeriod) + { + // Đảm bảo $startDate và $endDate là đối tượng Carbon + if (!($startDate instanceof Carbon)) { + $startDate = Carbon::parse($startDate); + } + + if (!($endDate instanceof Carbon)) { + $endDate = Carbon::parse($endDate); + } + + // Create an array to contain the results + $results = []; + + // Use CarbonPeriod to create a period from the start date to the end date + $period = CarbonPeriod::create($startDate, $endDate); + + $time_type = Category::where('c_type', 'TIME_TYPE')->get()->keyBy('c_code'); + $morning = $time_type->get('S'); + $afternoon = $time_type->get('C'); + $all_day = $time_type->get('ALL'); + + 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 + } + + foreach ($period as $date) { + // Bỏ qua Thứ 7 (6) và Chủ Nhật (0) + if ($date->dayOfWeek === Carbon::SATURDAY || $date->dayOfWeek === Carbon::SUNDAY) { + continue; + } + + if ($date->isSameDay($startDate)) { + //If the start date is morning, add afternoon + if ($startDate->isSameDay($endDate)) { // Nghỉ trong cùng 1 ngày + if ($startPeriod == $endPeriod) { // Cùng 1 buổi (S hoặc C) + $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod]; + } else { // Khác buổi (S đến C) -> cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } + } else { // Ngày bắt đầu khác ngày kết thúc + if ($startPeriod == $morning->c_code) { // Bắt đầu từ sáng -> tính cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } else { // Bắt đầu từ chiều -> tính buổi chiều + $results[] = ['date' => $date->toDateString(), 'period' => $startPeriod]; // Là $afternoon->c_code + } + } + } elseif ($date->isSameDay($endDate)) { // Ngày kết thúc (khác ngày bắt đầu) + if ($endPeriod == $afternoon->c_code) { // Kết thúc vào buổi chiều -> tính cả ngày + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } else { // Kết thúc vào buổi sáng -> tính buổi sáng + $results[] = ['date' => $date->toDateString(), 'period' => $endPeriod]; // Là $morning->c_code + } + } else { // Những ngày ở giữa + $results[] = ['date' => $date->toDateString(), 'period' => $all_day->c_code]; + } + } + + // Returns results + return $results; + } + private function getAllPeriod($startDate, $startPeriod, $endDate, $endPeriod) { //Create an array to contain the results @@ -488,4 +787,33 @@ class TicketController extends Controller //Returns results return $results; } + + /** + * Tính tổng số ngày nghỉ từ mảng các khoảng thời gian. + * 'ALL' = 1 ngày, 'S'/'C' = 0.5 ngày. + * + * @param array $dataListPeriod Mảng các khoảng thời gian nghỉ [['date' => 'Y-m-d', 'period' => 'ALL|S|C'], ...] + * @return float Tổng số ngày nghỉ + */ + private function calculateTotalLeaveDays(array $dataListPeriod): float + { + $totalDays = 0.0; + + foreach ($dataListPeriod as $periodData) { + if (isset($periodData['period'])) { + switch ($periodData['period']) { + case 'ALL': + $totalDays += 1.0; + break; + case 'S': // Buổi sáng + case 'C': // Buổi chiều + $totalDays += 0.5; + break; + // Có thể thêm default case để xử lý lỗi nếu cần + } + } + } + + return $totalDays; + } } diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 9f45755..f953769 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -2,6 +2,7 @@ import { getLeaveManagement, updateNoteLeave, exportLeaveManagement, + getListMaster, } from '@/api/Admin' import { update } from '@/rtk/helpers/CRUD' import { get, exportFile } from '@/rtk/helpers/apiService' @@ -57,6 +58,7 @@ interface MonthlyLeaveDays { leave_days: number month: number n_user_id: number + reason_code: string reason_name: string time_type_name: string } @@ -67,6 +69,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) @@ -97,6 +111,42 @@ const LeaveManagement = () => { const [date, setDate] = useState({ year: new Date().getFullYear().toString(), }) + const [dataTimeType, setDataTimeType] = useState([]) + const [dataReason, setDataReason] = useState([]) + + 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, { @@ -505,8 +555,16 @@ const LeaveManagement = () => { {data.map((user, index) => { let totalDayOff = 0 let totalDayLeave = - user.leaveDay.ld_day_total + user.leaveDay.ld_additional_day + user.leaveDay.ld_day_total + + user.leaveDay.ld_additional_day + + user.leaveDay.ld_special_leave_day + let ld_day_total = user.leaveDay.ld_day_total + let ld_additional_day = user.leaveDay.ld_additional_day + let ld_special_leave_day = user.leaveDay.ld_special_leave_day let ld_note = user.leaveDay.ld_note + let totalOnLeave = 0 + let totalLeaveWithoutPay = 0 + return ( {index + 1} @@ -539,6 +597,18 @@ const LeaveManagement = () => { const monthData = leaveDataByMonth[d.value] let total = monthData ? monthData.leave_days : 0 totalDayOff = totalDayOff + total + + user.monthlyLeaveDays + .filter((item) => item.month === d.value) + .map((item) => { + if (item.reason_code === 'ONLEAVE') { + totalOnLeave = totalOnLeave + Number(item.leave_days) + } else { + totalLeaveWithoutPay = + totalLeaveWithoutPay + Number(item.leave_days) + } + }) + return ( 0 ? '#ffb5b5' : ''} @@ -565,35 +635,70 @@ const LeaveManagement = () => { ) })} + {/* Total */} 0 ? '#92e6f2' : ''} + // bg={totalDayLeave > 0 ? '#92e6f2' : ''} > - {totalDayLeave} +

0 ? 'block' : 'none', + }} + > + {ld_day_total} +

+

0 ? 'block' : 'none', + }} + > + {ld_additional_day} +

+

0 ? 'block' : 'none', + }} + > + {ld_special_leave_day} +

- 0 ? '#ffb5b5' : ''}> + + {/* Off */} + {totalDayOff > 0 ? ( -

{totalDayOff}

+
+

+ {totalOnLeave} +

+

+ {totalLeaveWithoutPay} +

+
) : ( <> )}
+ + {/* Remaining */} 0 + : totalDayLeave - totalOnLeave > 0 ? '#c3ffc3' : '#ffb5b5' } > - {totalDayLeave - totalDayOff} + {totalDayLeave - totalOnLeave}