diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php index 270bae2..df0e629 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php @@ -524,8 +524,8 @@ class TicketController extends Controller $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) - if ($remainingOnleaveDaysInMonth <= 0) { + // 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; @@ -709,20 +709,35 @@ class TicketController extends Controller $totalAllocated = 0; if ($leaveDaysInfo) { - // if ($leaveDaysInfo->ld_day_total > $month) { - // $totalAllocated = $month; - // } else { + $currentMonth = Carbon::now()->month; $totalAllocated = $leaveDaysInfo->ld_day_total; - // } - // Nếu là duyệt ticket thì sẽ cộng thêm số ngày phép còn lại của tháng hiện tại - // if ($isAccept) { - if ($month >= $totalAllocated) { - $totalAllocated += $month - $totalAllocated; + // 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; + } } - // } - // bên hàm duyệt ticket sẽ check lại để + 1 ngày trước job để đảm bảo đủ ngày phép + // 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; + } + } } else { Log::warning("No LeaveDays record found for user ID: {$user->id}, year: {$year}. Assuming 0 allocated days."); } diff --git a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php index bb1e898..ff84f4d 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,36 @@ 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,7 +73,19 @@ 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(), ]); $user_res = [ @@ -98,8 +139,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/Kernel.php b/BACKEND/app/Console/Kernel.php index d8fa86b..f063914 100755 --- a/BACKEND/app/Console/Kernel.php +++ b/BACKEND/app/Console/Kernel.php @@ -25,7 +25,7 @@ class Kernel extends ConsoleKernel // ->dailyAt('18:00'); // Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm - // $schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59'); + $schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59'); $schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59'); // Chạy buổi sáng lúc 12:00 diff --git a/BACKEND/app/Jobs/AddMonthlyLeaveDays.php b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php index 4c58b9f..38fd483 100644 --- a/BACKEND/app/Jobs/AddMonthlyLeaveDays.php +++ b/BACKEND/app/Jobs/AddMonthlyLeaveDays.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Modules\Admin\app\Models\Category; class AddMonthlyLeaveDays implements ShouldQueue { @@ -18,6 +19,8 @@ class AddMonthlyLeaveDays implements ShouldQueue protected $month; protected $year; + private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng + public function __construct($month = null, $year = null) { $this->month = $month ?? Carbon::now()->month; @@ -29,6 +32,11 @@ class AddMonthlyLeaveDays implements ShouldQueue $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(); @@ -46,11 +54,36 @@ class AddMonthlyLeaveDays implements ShouldQueue ]); $leaveDay->save(); } else { - // Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại + // 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; + + // Xử lý ghi chú + $newNote = "Cập nhật ngày phép đến tháng " . $this->month; + if (!empty($leaveDay->ld_note)) { + // Nếu đã có ghi chú, thêm ghi chú mới vào và xuống dòng + $leaveDay->ld_note = $leaveDay->ld_note . "\n" . $newNote; + } else { + // Nếu chưa có ghi chú, gán ghi chú mới + $leaveDay->ld_note = $newNote; + } + $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ập nhật số ngày phép bằng với tháng hiện tại - $oldDays = $leaveDay->ld_day_total; - $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; // Xử lý ghi chú $newNote = "Cập nhật ngày phép đến tháng " . $this->month; diff --git a/BACKEND/app/Jobs/DeductLeaveDays.php b/BACKEND/app/Jobs/DeductLeaveDays.php index 2d4bd4a..b5f9571 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 @@ -42,38 +41,19 @@ class DeductLeaveDays implements ShouldQueue 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_additional_day > $totalLeaveDaysByMonth->leave_days) { - LeaveDays::where('ld_year', $this->year) - ->where('ld_user_id', $user->id) - ->update([ - 'ld_additional_day' => $totalLeaveDaysByMonth->leave_days, - ]); - } - } 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_additional_day' => "0", - ]); - } + ->where('n_user_id', $user->id) + ->where('n_year', $this->year) + ->where('n_month', "<=", 3) + ->where('n_reason', 'ONLEAVE') + ->sum('categories.c_value'); + + $existingData->ld_additional_day = $usedOnleaveDaysTotal ?? 0; + $existingData->save(); } } } diff --git a/BACKEND/app/Jobs/InitializeLeaveDays.php b/BACKEND/app/Jobs/InitializeLeaveDays.php index 1d7569f..6ba8114 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_total = 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) @@ -82,7 +83,7 @@ class InitializeLeaveDays implements ShouldQueue // Tạo dữ liệu cho năm hiện tại LeaveDays::insert([ 'ld_user_id' => $user->id, - 'ld_day_total' => $ld_day_total, + '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_additional_day' => $ld_additional_day, 'ld_note' => $ld_note, 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_24_025714_add_onleave_permanent_categories_table.php b/BACKEND/database/migrations/2025_06_24_025714_add_onleave_permanent_categories_table.php new file mode 100644 index 0000000..8aaa45f --- /dev/null +++ b/BACKEND/database/migrations/2025_06_24_025714_add_onleave_permanent_categories_table.php @@ -0,0 +1,35 @@ +insert([ + [ + 'c_code' => 'PERMANENT', + 'c_name' => 'Phép cộng nhân viên chính thức', + 'c_type' => 'PERMANENT_ONLEAVE', + 'c_value' => 3, + 'c_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('categories')->where('c_code', 'PERMANENT')->delete(); + } +}; diff --git a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx index f90a428..72f18da 100755 --- a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx +++ b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx @@ -654,12 +654,18 @@ const TicketsManagement = () => { onClick={() => { setIsRefuseConfirmOpen(false) }} + disabled={disableBtn} > Cancel diff --git a/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css b/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css index 6b56ab4..e3d17a8 100644 --- a/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css +++ b/FRONTEND/src/pages/UsersManagement/UsersManagement.module.css @@ -45,3 +45,37 @@ .dialogText { color: light-dark(#2d353c, white); } + +/* Thêm styles cho Modal xác nhận xóa */ +.deleteModal { + background-color: light-dark(white, #2d353c); + text-align: center; + border: solid 1px rgb(9, 132, 132); +} + +.deleteModalTitle { + color: rgb(9, 132, 132); + font-weight: 600; + font-size: 1.2rem; + margin-bottom: 1rem; +} + +.deleteModalContent { + color: light-dark(#2d353c, white); + margin-bottom: 1.5rem; +} + +.deleteModalFooter { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 1rem; +} + +.deleteButton { + background-color: rgb(9, 132, 132); +} + +.deleteButton:hover { + background-color: rgb(9, 132, 132); +} \ No newline at end of file diff --git a/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx b/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx index ba35dc5..0ee82a6 100644 --- a/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx +++ b/FRONTEND/src/pages/UsersManagement/UsersManagement.tsx @@ -13,6 +13,7 @@ import { Group, Modal, MultiSelect, + Switch, Text, TextInput, } from '@mantine/core' @@ -24,28 +25,31 @@ const UsersManagement = () => { const [users, setUsers] = useState([]) const [action, setAction] = useState('') const [activeBtn, setActiveBtn] = useState(false) - const [item, setItem] = useState({ id: 0 }) + const [item, setItem] = useState({ id: 0, is_permanent: false }) const [disableBtn, setDisableBtn] = useState(false) const [info, setInfo] = useState('') + const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] = + useState(false) + const columns = [ { name: 'id', - size: '3%', + size: '5%', header: 'ID', }, { name: 'name', - size: '17%', + size: '20%', header: 'Name', }, { name: 'email', - size: '26%', + size: '25%', header: 'Email', }, { name: 'permission', - size: '10%', + size: '20%', header: 'Permission', render: (row: TUser) => { if (row.permission.includes(',')) { @@ -57,9 +61,21 @@ const UsersManagement = () => { } }, }, + { + name: 'is_permanent', + size: '20%', + header: 'Employment Type', + render: (row: TUser) => { + return row.is_permanent ? ( + Permanent + ) : ( + Probation + ) + }, + }, { name: '#', - size: '5%', + size: '10%', header: 'Action', render: (row: TUser) => { return ( @@ -68,6 +84,8 @@ const UsersManagement = () => { className={classes.editIcon} onClick={() => { setAction('edit') + setItem(row) + form.reset() form.setValues(row) }} width={20} @@ -94,6 +112,7 @@ const UsersManagement = () => { name: '', email: '', permission: '', + is_permanent: false, }, }) @@ -128,6 +147,7 @@ const UsersManagement = () => { const res = await update(createOrUpdateUser, values, getAll) if (res === true) { setAction('') + setIsPermanentConfirmOpen(false) form.reset() } } catch (error) { @@ -172,6 +192,7 @@ const UsersManagement = () => { opened={action === 'add' || action === 'edit'} onClose={() => { setAction('') + setIsPermanentConfirmOpen(false) form.reset() }} title={ @@ -183,9 +204,15 @@ const UsersManagement = () => {
{ setDisableBtn(true) - action === 'edit' - ? await handleUpdate(values) - : await handleCreate(values) + if (action === 'edit') { + if (values.is_permanent && !item.is_permanent) { + setIsPermanentConfirmOpen(true) + } else { + await handleUpdate(values) + } + } else { + await handleCreate(values) + } setDisableBtn(false) })} > @@ -196,6 +223,7 @@ const UsersManagement = () => { value={form.values.name} error={form.errors.name} onChange={(e) => form.setFieldValue('name', e.target.value)} + required /> { value={form.values.email} error={form.errors.email} onChange={(e) => form.setFieldValue('email', e.target.value)} + required /> { e!.filter((p) => p.trim() !== '').join(','), ) } + mb={'md'} /> + + {action === 'edit' && !item.is_permanent ? ( + + form.setFieldValue( + 'is_permanent', + event.currentTarget.checked, + ) + } + /> + ) : ( + '' + )} + {action === 'add' ? ( + + + +