diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index 6e122bc..a0d8286 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -7,13 +7,18 @@ use App\Http\Controllers\Controller; use App\Jobs\InitializeLeaveDays; use App\Models\LeaveDays; use App\Models\Notes; +use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; use Maatwebsite\Excel\Facades\Excel; +use Modules\Admin\app\Models\Ticket; +use App\Traits\AnalyzeData; class LeaveManagementController extends Controller { + use AnalyzeData; + public function get(Request $request) { $yearNow = $request->query('year', now()->year); @@ -166,6 +171,16 @@ class LeaveManagementController extends Controller $note->n_reason = $reason; $note->save(); + + // Clear Timekeeping cache + $ticket = Ticket::find($note->ticket_id); + if ($ticket) { + $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); + } else { + $this->createOrUpdateRecordForCurrentMonth(Carbon::parse($note->created_at)->month, Carbon::parse($note->created_at)->year); + } + return response()->json(data: ['message' => 'Update success', 'status' => true]); } diff --git a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php index 565b368..5120b52 100755 --- a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php +++ b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php @@ -39,7 +39,7 @@ class UserController extends Controller ]); if ($request->has('id')) { - $payload = $request->only(['name', 'email', 'permission', 'is_permanent']); + $payload = $request->only(['name', 'email', 'permission', 'is_permanent', 'is_separated']); $user = User::find($request->id); // Không cho chuyển từ chính thức thành lại thử việc @@ -47,6 +47,10 @@ class UserController extends Controller return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']); } + if (!$request->is_separated && $user->is_separated) { + return response()->json(['status' => false, 'message' => 'You cannot change status of separated employee.']); + } + // 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) @@ -78,7 +82,8 @@ class UserController extends Controller 'email' => $request->email, 'password' => bcrypt('Work@1234'), 'permission' => $request->permission, - 'is_permanent' => false + 'is_permanent' => false, + 'is_separated' => false ]); // Khởi tạo LeaveDays cho nhân viên mới diff --git a/BACKEND/Modules/Auth/app/Models/User.php b/BACKEND/Modules/Auth/app/Models/User.php index 4347eca..d6a7de4 100755 --- a/BACKEND/Modules/Auth/app/Models/User.php +++ b/BACKEND/Modules/Auth/app/Models/User.php @@ -27,6 +27,7 @@ class User extends Authenticatable implements JWTSubject 'password', 'permission', 'is_permanent', + 'is_separated', 'permanent_date' ]; diff --git a/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php b/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php index bf74a72..b68abf8 100644 --- a/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php +++ b/BACKEND/app/Console/Commands/InitializeLeaveDaysCommand.php @@ -17,8 +17,6 @@ class InitializeLeaveDaysCommand extends Command public function handle() { - $year = $this->argument('year'); - // Không sử dụng nữa, theo rule mới - // InitializeLeaveDays::dispatch($year); + InitializeLeaveDays::dispatch(); } } diff --git a/BACKEND/app/Jobs/AddMonthlyLeaveDays.php b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php index 15372f4..e70192b 100644 --- a/BACKEND/app/Jobs/AddMonthlyLeaveDays.php +++ b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php @@ -37,6 +37,11 @@ class AddMonthlyLeaveDays implements ShouldQueue continue; } + // Nếu là nhân viên nghỉ việc, ko cộng phép + if ($user->is_separated) { + continue; + } + $leaveDay = LeaveDays::where('ld_user_id', $user->id) ->where('ld_year', $this->year) ->first(); diff --git a/BACKEND/app/Jobs/CheckUserAttendanceJob.php b/BACKEND/app/Jobs/CheckUserAttendanceJob.php index fe42331..7be15c7 100644 --- a/BACKEND/app/Jobs/CheckUserAttendanceJob.php +++ b/BACKEND/app/Jobs/CheckUserAttendanceJob.php @@ -38,11 +38,14 @@ class CheckUserAttendanceJob implements ShouldQueue { // Lấy tất cả người dùng $users = User::where('permission', 'not like', '%admin%') - ->where('permission', 'not like', '%accountant%')->get(); + ->where('permission', 'not like', '%accountant%')->get(); + foreach ($users as $key => $user) { - // if ($user->id != 4) { - // continue; - // } + // Check nhân viên nghỉ việc + if ($user->is_separated) { + continue; + } + // Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C') if ($this->period === 'S') { $this->checkMorning($user); @@ -128,14 +131,19 @@ class CheckUserAttendanceJob implements ShouldQueue $reason = 'KHONG PHEP'; //Check ngày hợp lệ nếu có check và check out của user khác - $dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime]) - ->get(); - $hasCheckIndateNow = $dateNow->where('status', 'check in')->isNotEmpty(); - $hasCheckOutdateNow = $dateNow->where('status', 'check out')->isNotEmpty(); + $dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])->exists(); - // Nếu không có check-in hoặc check-out, và chưa có ticket, tạo ticket mới - if (($hasCheckIndateNow && $hasCheckOutdateNow) && !$hasCheckIn && !$hasCheckOut && !$existingTicket) { - Ticket::create([ + // Nếu không có check in/out, không gửi ticket và có check in/out của người khác => tạo ticket không phép + if ($hasCheckIn || $hasCheckOut) { + return; + } + + if ($existingTicket) { + return; + } + + if ($dateNow) { + $ticket = Ticket::create([ 'user_id' => $userId, 'start_date' => $today->format('Y-m-d'), 'start_period' => $periodCode, @@ -159,8 +167,10 @@ class CheckUserAttendanceJob implements ShouldQueue $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, diff --git a/BACKEND/app/Jobs/InitializeLeaveDays.php b/BACKEND/app/Jobs/InitializeLeaveDays.php index 424745c..5f40b47 100644 --- a/BACKEND/app/Jobs/InitializeLeaveDays.php +++ b/BACKEND/app/Jobs/InitializeLeaveDays.php @@ -37,6 +37,11 @@ class InitializeLeaveDays implements ShouldQueue $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) { + // Check nhân viên nghỉ việc + if ($user->is_separated) { + continue; + } + // 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) ->where('ld_year', $this->year) diff --git a/BACKEND/database/migrations/2025_08_05_098764_add_separated_users_table.php b/BACKEND/database/migrations/2025_08_05_098764_add_separated_users_table.php new file mode 100644 index 0000000..716823e --- /dev/null +++ b/BACKEND/database/migrations/2025_08_05_098764_add_separated_users_table.php @@ -0,0 +1,29 @@ +boolean('is_separated')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_separated'); + }); + } +}; diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 1791eed..963e2b0 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -853,7 +853,13 @@ const LeaveManagement = () => { return ( 0 ? '#ffb5b5' : ''} - opacity={d.value > currentMonth ? 0.4 : 1} + opacity={ + Number(date.year) === currentYear + ? d.value > currentMonth + ? 0.4 + : 1 + : 1 + } key={i} ta={'center'} > @@ -974,6 +980,7 @@ const LeaveManagement = () => { {/* Off */} { openDetailOff() setDetailOffItem(user) diff --git a/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css b/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css index e3d17a8..61b9948 100644 --- a/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css +++ b/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css @@ -47,19 +47,52 @@ } /* Thêm styles cho Modal xác nhận xóa */ -.deleteModal { +.confirmModal { background-color: light-dark(white, #2d353c); text-align: center; border: solid 1px rgb(9, 132, 132); } -.deleteModalTitle { +.confirmModalTitle { color: rgb(9, 132, 132); font-weight: 600; font-size: 1.2rem; margin-bottom: 1rem; } +.confirmModalContent { + color: light-dark(#2d353c, white); + margin-bottom: 1.5rem; +} + +.confirmModalFooter { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 1rem; +} + +.confirmButton { + background-color: rgb(9, 132, 132); +} + +.confirmButton:hover { + background-color: rgb(9, 132, 132); +} + +.deleteModal { + background-color: light-dark(white, #2d353c); + text-align: center; + border: solid 1px #ff4646; +} + +.deleteModalTitle { + color: #ff4646; + font-weight: 600; + font-size: 1.2rem; + margin-bottom: 1rem; +} + .deleteModalContent { color: light-dark(#2d353c, white); margin-bottom: 1.5rem; @@ -73,9 +106,9 @@ } .deleteButton { - background-color: rgb(9, 132, 132); + background-color: #ff4646; } .deleteButton:hover { - background-color: rgb(9, 132, 132); + background-color: #ff6b6b; } \ No newline at end of file diff --git a/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx b/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx index 0ee82a6..cb23a89 100644 --- a/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx +++ b/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx @@ -13,7 +13,7 @@ import { Group, Modal, MultiSelect, - Switch, + Select, Text, TextInput, } from '@mantine/core' @@ -21,15 +21,30 @@ import { useForm } from '@mantine/form' import { IconEdit, IconX } from '@tabler/icons-react' import { useEffect, useState } from 'react' import classes from './UsersManagement.module.css' + +type TUserForm = { + id: number + email: string + name: string + permission: string + status: string +} + const UsersManagement = () => { const [users, setUsers] = useState([]) const [action, setAction] = useState('') const [activeBtn, setActiveBtn] = useState(false) - const [item, setItem] = useState({ id: 0, is_permanent: false }) + const [item, setItem] = useState({ + id: 0, + is_permanent: false, + is_separated: false, + }) const [disableBtn, setDisableBtn] = useState(false) const [info, setInfo] = useState('') const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] = useState(false) + const [isSeparatedConfirmOpen, setIsSeparatedConfirmOpen] = + useState(false) const columns = [ { @@ -66,6 +81,10 @@ const UsersManagement = () => { size: '20%', header: 'Employment Type', render: (row: TUser) => { + if (row.is_separated) { + return Separated + } + return row.is_permanent ? ( Permanent ) : ( @@ -86,7 +105,10 @@ const UsersManagement = () => { setAction('edit') setItem(row) form.reset() - form.setValues(row) + form.setValues({ + ...row, + status: row.is_permanent ? 'permanent' : '', + }) }} width={20} height={20} @@ -112,7 +134,7 @@ const UsersManagement = () => { name: '', email: '', permission: '', - is_permanent: false, + status: '', }, }) @@ -127,7 +149,7 @@ const UsersManagement = () => { } } - const handleCreate = async (values: TUser) => { + const handleCreate = async (values: TUserForm) => { try { const { id, ...data } = values const res = await post(createOrUpdateUser, data) @@ -142,12 +164,25 @@ const UsersManagement = () => { } } - const handleUpdate = async (values: TUser) => { + const handleUpdate = async (values: TUserForm) => { try { - const res = await update(createOrUpdateUser, values, getAll) + const res = await update( + createOrUpdateUser, + { + ...values, + is_permanent: item.is_permanent + ? true + : values.status === 'permanent', + is_separated: item.is_separated + ? true + : values.status === 'separated', + }, + getAll, + ) if (res === true) { setAction('') setIsPermanentConfirmOpen(false) + setIsSeparatedConfirmOpen(false) form.reset() } } catch (error) { @@ -193,6 +228,7 @@ const UsersManagement = () => { onClose={() => { setAction('') setIsPermanentConfirmOpen(false) + setIsSeparatedConfirmOpen(false) form.reset() }} title={ @@ -205,8 +241,10 @@ const UsersManagement = () => { onSubmit={form.onSubmit(async (values) => { setDisableBtn(true) if (action === 'edit') { - if (values.is_permanent && !item.is_permanent) { + if (values.status === 'permanent' && !item.is_permanent) { setIsPermanentConfirmOpen(true) + } else if (values.status === 'separated' && !item.is_separated) { + setIsSeparatedConfirmOpen(true) } else { await handleUpdate(values) } @@ -256,17 +294,21 @@ const UsersManagement = () => { mb={'md'} /> - {action === 'edit' && !item.is_permanent ? ( - - form.setFieldValue( - 'is_permanent', - event.currentTarget.checked, - ) - } + {action === 'edit' && !item.is_separated ? ( +