update handle for separated user

This commit is contained in:
dbdbd9 2025-08-05 15:14:50 +07:00
parent a4751e9d38
commit d335803cf8
12 changed files with 237 additions and 43 deletions

View File

@ -7,13 +7,18 @@ use App\Http\Controllers\Controller;
use App\Jobs\InitializeLeaveDays; use App\Jobs\InitializeLeaveDays;
use App\Models\LeaveDays; use App\Models\LeaveDays;
use App\Models\Notes; use App\Models\Notes;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use Modules\Admin\app\Models\Ticket;
use App\Traits\AnalyzeData;
class LeaveManagementController extends Controller class LeaveManagementController extends Controller
{ {
use AnalyzeData;
public function get(Request $request) public function get(Request $request)
{ {
$yearNow = $request->query('year', now()->year); $yearNow = $request->query('year', now()->year);
@ -166,6 +171,16 @@ class LeaveManagementController extends Controller
$note->n_reason = $reason; $note->n_reason = $reason;
$note->save(); $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]); return response()->json(data: ['message' => 'Update success', 'status' => true]);
} }

View File

@ -39,7 +39,7 @@ class UserController extends Controller
]); ]);
if ($request->has('id')) { 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); $user = User::find($request->id);
// Không cho chuyển từ chính thức thành lại thử việc // 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.']); 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 // Thêm ngày phép khi thành nhân viên chính thức
if ($request->is_permanent && !$user->is_permanent) { if ($request->is_permanent && !$user->is_permanent) {
$userLeaveDay = LeaveDays::where('ld_user_id', $user->id) $userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
@ -78,7 +82,8 @@ class UserController extends Controller
'email' => $request->email, 'email' => $request->email,
'password' => bcrypt('Work@1234'), 'password' => bcrypt('Work@1234'),
'permission' => $request->permission, 'permission' => $request->permission,
'is_permanent' => false 'is_permanent' => false,
'is_separated' => false
]); ]);
// Khởi tạo LeaveDays cho nhân viên mới // Khởi tạo LeaveDays cho nhân viên mới

View File

@ -27,6 +27,7 @@ class User extends Authenticatable implements JWTSubject
'password', 'password',
'permission', 'permission',
'is_permanent', 'is_permanent',
'is_separated',
'permanent_date' 'permanent_date'
]; ];

View File

@ -17,8 +17,6 @@ class InitializeLeaveDaysCommand extends Command
public function handle() public function handle()
{ {
$year = $this->argument('year'); InitializeLeaveDays::dispatch();
// Không sử dụng nữa, theo rule mới
// InitializeLeaveDays::dispatch($year);
} }
} }

View File

@ -37,6 +37,11 @@ class AddMonthlyLeaveDays implements ShouldQueue
continue; 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) $leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year) ->where('ld_year', $this->year)
->first(); ->first();

View File

@ -38,11 +38,14 @@ class CheckUserAttendanceJob implements ShouldQueue
{ {
// Lấy tất cả người dùng // Lấy tất cả người dùng
$users = User::where('permission', 'not like', '%admin%') $users = User::where('permission', 'not like', '%admin%')
->where('permission', 'not like', '%accountant%')->get(); ->where('permission', 'not like', '%accountant%')->get();
foreach ($users as $key => $user) { foreach ($users as $key => $user) {
// if ($user->id != 4) { // Check nhân viên nghỉ việc
// continue; if ($user->is_separated) {
// } continue;
}
// Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C') // Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C')
if ($this->period === 'S') { if ($this->period === 'S') {
$this->checkMorning($user); $this->checkMorning($user);
@ -128,14 +131,19 @@ class CheckUserAttendanceJob implements ShouldQueue
$reason = 'KHONG PHEP'; $reason = 'KHONG PHEP';
//Check ngày hợp lệ nếu có check và check out của user khác //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]) $dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])->exists();
->get();
$hasCheckIndateNow = $dateNow->where('status', 'check in')->isNotEmpty();
$hasCheckOutdateNow = $dateNow->where('status', 'check out')->isNotEmpty();
// Nếu không có check-in hoặc check-out, và chưa có ticket, tạo ticket mới // 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 (($hasCheckIndateNow && $hasCheckOutdateNow) && !$hasCheckIn && !$hasCheckOut && !$existingTicket) { if ($hasCheckIn || $hasCheckOut) {
Ticket::create([ return;
}
if ($existingTicket) {
return;
}
if ($dateNow) {
$ticket = Ticket::create([
'user_id' => $userId, 'user_id' => $userId,
'start_date' => $today->format('Y-m-d'), 'start_date' => $today->format('Y-m-d'),
'start_period' => $periodCode, 'start_period' => $periodCode,
@ -159,8 +167,10 @@ class CheckUserAttendanceJob implements ShouldQueue
$admins = Admin::where('permission', 'like', '%admin%')->get(); $admins = Admin::where('permission', 'like', '%admin%')->get();
foreach ($admins as $key => $value) { foreach ($admins as $key => $value) {
$data = array( $data = array(
"ticket_id" => $ticket->id,
"email_template" => "email.notification_tickets", "email_template" => "email.notification_tickets",
"email" => $user->email, "email" => $user->email,
"admin_email" => $value->email,
"name" => $user->name, "name" => $user->name,
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")", "date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
"type" => $dataMasterType->c_name, "type" => $dataMasterType->c_name,

View File

@ -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 $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) { 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 // 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) $existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year) ->where('ld_year', $this->year)

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_separated')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_separated');
});
}
};

View File

@ -853,7 +853,13 @@ const LeaveManagement = () => {
return ( return (
<Table.Td <Table.Td
bg={total > 0 ? '#ffb5b5' : ''} bg={total > 0 ? '#ffb5b5' : ''}
opacity={d.value > currentMonth ? 0.4 : 1} opacity={
Number(date.year) === currentYear
? d.value > currentMonth
? 0.4
: 1
: 1
}
key={i} key={i}
ta={'center'} ta={'center'}
> >
@ -974,6 +980,7 @@ const LeaveManagement = () => {
{/* Off */} {/* Off */}
<Table.Td <Table.Td
style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
openDetailOff() openDetailOff()
setDetailOffItem(user) setDetailOffItem(user)

View File

@ -47,19 +47,52 @@
} }
/* Thêm styles cho Modal xác nhận xóa */ /* Thêm styles cho Modal xác nhận xóa */
.deleteModal { .confirmModal {
background-color: light-dark(white, #2d353c); background-color: light-dark(white, #2d353c);
text-align: center; text-align: center;
border: solid 1px rgb(9, 132, 132); border: solid 1px rgb(9, 132, 132);
} }
.deleteModalTitle { .confirmModalTitle {
color: rgb(9, 132, 132); color: rgb(9, 132, 132);
font-weight: 600; font-weight: 600;
font-size: 1.2rem; font-size: 1.2rem;
margin-bottom: 1rem; 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 { .deleteModalContent {
color: light-dark(#2d353c, white); color: light-dark(#2d353c, white);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
@ -73,9 +106,9 @@
} }
.deleteButton { .deleteButton {
background-color: rgb(9, 132, 132); background-color: #ff4646;
} }
.deleteButton:hover { .deleteButton:hover {
background-color: rgb(9, 132, 132); background-color: #ff6b6b;
} }

View File

@ -13,7 +13,7 @@ import {
Group, Group,
Modal, Modal,
MultiSelect, MultiSelect,
Switch, Select,
Text, Text,
TextInput, TextInput,
} from '@mantine/core' } from '@mantine/core'
@ -21,15 +21,30 @@ import { useForm } from '@mantine/form'
import { IconEdit, IconX } from '@tabler/icons-react' import { IconEdit, IconX } from '@tabler/icons-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import classes from './UsersManagement.module.css' import classes from './UsersManagement.module.css'
type TUserForm = {
id: number
email: string
name: string
permission: string
status: string
}
const UsersManagement = () => { const UsersManagement = () => {
const [users, setUsers] = useState<TUser[]>([]) const [users, setUsers] = useState<TUser[]>([])
const [action, setAction] = useState('') const [action, setAction] = useState('')
const [activeBtn, setActiveBtn] = useState(false) 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 [disableBtn, setDisableBtn] = useState(false)
const [info, setInfo] = useState('') const [info, setInfo] = useState('')
const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] = const [isPermanentConfirmOpen, setIsPermanentConfirmOpen] =
useState<boolean>(false) useState<boolean>(false)
const [isSeparatedConfirmOpen, setIsSeparatedConfirmOpen] =
useState<boolean>(false)
const columns = [ const columns = [
{ {
@ -66,6 +81,10 @@ const UsersManagement = () => {
size: '20%', size: '20%',
header: 'Employment Type', header: 'Employment Type',
render: (row: TUser) => { render: (row: TUser) => {
if (row.is_separated) {
return <Badge color="red">Separated</Badge>
}
return row.is_permanent ? ( return row.is_permanent ? (
<Badge color="teal">Permanent</Badge> <Badge color="teal">Permanent</Badge>
) : ( ) : (
@ -86,7 +105,10 @@ const UsersManagement = () => {
setAction('edit') setAction('edit')
setItem(row) setItem(row)
form.reset() form.reset()
form.setValues(row) form.setValues({
...row,
status: row.is_permanent ? 'permanent' : '',
})
}} }}
width={20} width={20}
height={20} height={20}
@ -112,7 +134,7 @@ const UsersManagement = () => {
name: '', name: '',
email: '', email: '',
permission: '', permission: '',
is_permanent: false, status: '',
}, },
}) })
@ -127,7 +149,7 @@ const UsersManagement = () => {
} }
} }
const handleCreate = async (values: TUser) => { const handleCreate = async (values: TUserForm) => {
try { try {
const { id, ...data } = values const { id, ...data } = values
const res = await post(createOrUpdateUser, data) const res = await post(createOrUpdateUser, data)
@ -142,12 +164,25 @@ const UsersManagement = () => {
} }
} }
const handleUpdate = async (values: TUser) => { const handleUpdate = async (values: TUserForm) => {
try { 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) { if (res === true) {
setAction('') setAction('')
setIsPermanentConfirmOpen(false) setIsPermanentConfirmOpen(false)
setIsSeparatedConfirmOpen(false)
form.reset() form.reset()
} }
} catch (error) { } catch (error) {
@ -193,6 +228,7 @@ const UsersManagement = () => {
onClose={() => { onClose={() => {
setAction('') setAction('')
setIsPermanentConfirmOpen(false) setIsPermanentConfirmOpen(false)
setIsSeparatedConfirmOpen(false)
form.reset() form.reset()
}} }}
title={ title={
@ -205,8 +241,10 @@ const UsersManagement = () => {
onSubmit={form.onSubmit(async (values) => { onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true) setDisableBtn(true)
if (action === 'edit') { if (action === 'edit') {
if (values.is_permanent && !item.is_permanent) { if (values.status === 'permanent' && !item.is_permanent) {
setIsPermanentConfirmOpen(true) setIsPermanentConfirmOpen(true)
} else if (values.status === 'separated' && !item.is_separated) {
setIsSeparatedConfirmOpen(true)
} else { } else {
await handleUpdate(values) await handleUpdate(values)
} }
@ -256,17 +294,21 @@ const UsersManagement = () => {
mb={'md'} mb={'md'}
/> />
{action === 'edit' && !item.is_permanent ? ( {action === 'edit' && !item.is_separated ? (
<Switch <Select
label="Permanent employee" label="Employment type"
style={{ width: 'fit-content' }} data={[
checked={form.values.is_permanent} { value: 'permanent', label: 'Permanent' },
onChange={(event) => { value: 'separated', label: 'Separated' },
form.setFieldValue( ]}
'is_permanent', value={form.values.status}
event.currentTarget.checked, onChange={(value) => {
) if (value) {
} form.setFieldValue('status', value)
}
}}
w="50%"
required
/> />
) : ( ) : (
'' ''
@ -327,14 +369,57 @@ const UsersManagement = () => {
onClose={() => setIsPermanentConfirmOpen(false)} onClose={() => setIsPermanentConfirmOpen(false)}
centered centered
size="sm" size="sm"
classNames={{
content: classes.confirmModal,
}}
>
<Text className={classes.confirmModalTitle}>Confirm Update</Text>
<Text className={classes.confirmModalContent}>
This action will change the employment type from{' '}
<strong>Probation</strong> to <strong>Permanent</strong>.
</Text>
<Text className={classes.confirmModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.confirmModalFooter}>
<Button
variant="outline"
onClick={() => {
setIsPermanentConfirmOpen(false)
}}
disabled={disableBtn}
>
Cancel
</Button>
<Button
className={classes.confirmButton}
onClick={async () => {
setDisableBtn(true)
await handleUpdate(form.values)
setDisableBtn(false)
}}
disabled={disableBtn}
>
Confirm
</Button>
</Box>
</Modal>
{/* Confirm change to separated employee */}
<Modal
opened={isSeparatedConfirmOpen}
onClose={() => setIsSeparatedConfirmOpen(false)}
centered
size="sm"
classNames={{ classNames={{
content: classes.deleteModal, content: classes.deleteModal,
}} }}
> >
<Text className={classes.deleteModalTitle}>Confirm Update</Text> <Text className={classes.deleteModalTitle}>Confirm Update</Text>
<Text className={classes.deleteModalContent}> <Text className={classes.deleteModalContent}>
This action will change the employment type from{' '} This action will change the employment type to{' '}
<strong>Probation</strong> to <strong>Permanent</strong>. <strong>Separated</strong>.
</Text> </Text>
<Text className={classes.deleteModalContent}> <Text className={classes.deleteModalContent}>
Are you sure you want to proceed? Are you sure you want to proceed?
@ -344,7 +429,7 @@ const UsersManagement = () => {
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setIsPermanentConfirmOpen(false) setIsSeparatedConfirmOpen(false)
}} }}
disabled={disableBtn} disabled={disableBtn}
> >

View File

@ -77,6 +77,7 @@ export type TUser = {
name: string name: string
permission: string permission: string
is_permanent: boolean is_permanent: boolean
is_separated: boolean
} }
export type DataReason = { export type DataReason = {