add update note status

This commit is contained in:
dbdbd9 2025-07-01 13:40:48 +07:00
parent 705e8f9216
commit d064b242e6
8 changed files with 459 additions and 41 deletions

View File

@ -38,6 +38,7 @@ class LeaveManagementController extends Controller
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
})
->select(
DB::raw('notes.id as id'),
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_time_type as time_type'),
DB::raw('notes.n_year as year'),
@ -58,6 +59,7 @@ class LeaveManagementController extends Controller
->get()
->map(function ($item) {
return [
"id" => $item->id,
"day" => $item->day,
"n_user_id" => $item->n_user_id,
"reason_code" => $item->reason_code,
@ -145,6 +147,28 @@ class LeaveManagementController extends Controller
return response()->json(['status' => true, 'message' => 'Updated successfully']);
}
public function updateNoteStatus(Request $request)
{
$rules = [
'id' => 'required',
'n_reason' => 'required|in:ONLEAVE,LEAVE_WITHOUT_PAY'
];
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$reason = $request->input('n_reason');
$note = Notes::find($id);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
$note->n_reason = $reason;
$note->save();
return response()->json(data: ['message' => 'Update success', 'status' => true]);
}
public function export(Request $request)
{
$year = $request->query('year', now()->year);

View File

@ -173,52 +173,62 @@ 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) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
$ticket = Ticket::find($note->ticket_id);
if (!$ticket) {
return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
if ($note->ticket_id != null) {
$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]);
}
$admin = auth('admins')->user();
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
// 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]);
}

View File

@ -157,6 +157,7 @@ Route::middleware('api')
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
Route::post('/updateNoteStatus', [LeaveManagementController::class, 'updateNoteStatus'])->middleware('check.permission:admin.hr');
});
Route::group([

View File

@ -13,10 +13,13 @@ use Modules\Auth\app\Models\User;
use Illuminate\Support\Str;
use Modules\Admin\app\Models\Category;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use App\Traits\AnalyzeData;
class UserController extends Controller
{
use IsAPI;
use AnalyzeData;
public function __construct()
{
$this->middleware('jwt.auth');

View File

@ -40,6 +40,8 @@ export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
export const updateNoteLeave =
API_URL + 'v1/admin/leave-management/saveNoteLeave'
export const updateNoteStatus =
API_URL + 'v1/admin/leave-management/updateNoteStatus'
export const exportLeaveManagement =
API_URL + 'v1/admin/leave-management/export'

View File

@ -60,3 +60,37 @@
padding-top: 5px;
padding-bottom: 5px;
}
/* Thêm styles cho Modal xác nhận xóa */
.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;
}
.deleteModalFooter {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 1rem;
}
.deleteButton {
background-color: #ff4646;
}
.deleteButton:hover {
background-color: #ff6b6b;
}

View File

@ -11,6 +11,7 @@ import {
Group,
HoverCard,
Menu,
Modal,
Select,
Stack,
Table,
@ -21,15 +22,23 @@ import {
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import { IconEdit, IconFileExcel, IconHelpCircle } from '@tabler/icons-react'
import {
IconEdit,
IconFileExcel,
IconHelpCircle,
IconRefresh,
IconTrash,
} from '@tabler/icons-react'
import classes from './LeaveManagement.module.css'
import {
getLeaveManagement,
updateNoteLeave,
exportLeaveManagement,
updateNoteStatus,
deleteNote,
} from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { get, exportFile } from '@/rtk/helpers/apiService'
interface User {
@ -57,6 +66,7 @@ interface LeaveDay {
}
interface MonthlyLeaveDays {
id: number
day: number
leave_days: number
month: number
@ -86,6 +96,48 @@ interface UserData {
const LeaveManagement = () => {
const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
const [openedDetailOff, { open: openDetailOff, close: closeDetailOff }] =
useDisclosure(false)
const [detailOffItem, setDetailOffItem] = useState<UserData>({
user: {
id: 0,
name: '',
email: '',
email_verified_at: '',
permission: '',
remember_token: '',
avatar: '',
created_at: '',
updated_at: '',
},
leaveDay: {
id: 0,
ld_user_id: 0,
ld_year: 0,
ld_day_total: 0,
ld_additional_day: 0,
ld_special_leave_day: 0,
ld_note: '',
created_at: '',
updated_at: '',
},
monthlyLeaveDays: [],
})
const [isStatusConfirmOpen, setIsStatusConfirmOpen] = useState(false)
const [isDisableStatusBtn, setIsDisableStatusBtn] = useState(false)
const [noteStatus, setNoteStatus] = useState<{
id: number
reason: string
}>({
id: 0,
reason: '',
})
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false)
const [isDisableDeleteBtn, setIsDisableDeleteBtn] = useState(false)
const [noteToDelete, setNoteToDelete] = useState<any>(null)
const [disableBtn, setDisableBtn] = useState(false)
const monthInYear = getMonthNames()
const [customAddNotes, setCustomAddNotes] = useState<{
@ -209,6 +261,51 @@ const LeaveManagement = () => {
}
}
const updateNote = async () => {
setIsDisableStatusBtn(true)
try {
await update(
updateNoteStatus,
{
id: noteStatus.id,
n_reason: noteStatus.reason,
},
getLeaveList,
)
} catch (error) {
console.log(error)
} finally {
setIsDisableStatusBtn(false)
closeDetailOff()
setIsStatusConfirmOpen(false)
setNoteStatus({
id: 0,
reason: '',
})
}
}
const handleDeleteNote = async () => {
if (noteToDelete) {
setIsDisableDeleteBtn(true)
try {
await Xdelete(
deleteNote,
{ id: noteToDelete.id, month: noteToDelete.month, year: date.year },
getLeaveList,
)
} catch (error) {
console.log(error)
} finally {
setIsDisableDeleteBtn(false)
closeDetailOff()
setIsDeleteConfirmOpen(false)
setNoteToDelete(null)
}
}
}
function getMonthNames() {
const monthNames = [
{
@ -831,7 +928,12 @@ const LeaveManagement = () => {
</Table.Td>
{/* Off */}
<Table.Td>
<Table.Td
onClick={() => {
openDetailOff()
setDetailOffItem(user)
}}
>
{totalDayOff > 0 ? (
<Tooltip
multiline
@ -949,6 +1051,248 @@ const LeaveManagement = () => {
</Table.Tbody>
</Table>
</Box>
<Modal
opened={openedDetailOff}
onClose={() => {
closeDetailOff()
setDetailOffItem({
user: {
id: 0,
name: '',
email: '',
email_verified_at: '',
permission: '',
remember_token: '',
avatar: '',
created_at: '',
updated_at: '',
},
leaveDay: {
id: 0,
ld_user_id: 0,
ld_year: 0,
ld_day_total: 0,
ld_additional_day: 0,
ld_special_leave_day: 0,
ld_note: '',
created_at: '',
updated_at: '',
},
monthlyLeaveDays: [],
})
}}
title={
<Stack>
<Text size="lg" fw={600}>
Chi tiết nghỉ phép {date.year}
</Text>
<Text size="md" c="dimmed">
Nhân viên:{' '}
<Text span fw={700} c="dark">
{detailOffItem.user.name}
</Text>
</Text>
</Stack>
}
size="70%"
>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>Time</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>Action</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{detailOffItem.monthlyLeaveDays
.slice()
.reverse()
.map((item: MonthlyLeaveDays) => {
return (
<Table.Tr key={item.id}>
<Table.Td>
{item.time_type_name} ({item.day}/{item.month})
</Table.Td>
<Table.Td>
<Badge
color={
item.reason_code === 'ONLEAVE'
? 'teal'
: item.reason_code === 'LEAVE_WITHOUT_PAY'
? 'red'
: 'blue'
}
>
{item.reason_name}
</Badge>
</Table.Td>
<Table.Td>
{item.reason_code === 'ONLEAVE' ? (
<IconRefresh
className={classes.deleteIcon}
onClick={() => {
setIsStatusConfirmOpen(true)
setNoteStatus({
id: item.id,
reason: 'LEAVE_WITHOUT_PAY',
})
}}
title="Chuyển thành không phép"
width={20}
height={20}
/>
) : item.reason_code === 'LEAVE_WITHOUT_PAY' ? (
<IconRefresh
className={classes.editIcon}
onClick={() => {
setIsStatusConfirmOpen(true)
setNoteStatus({
id: item.id,
reason: 'ONLEAVE',
})
}}
title="Chuyển thành có phép"
width={20}
height={20}
/>
) : (
''
)}
<IconTrash
className={classes.deleteIcon}
onClick={() => {
setNoteToDelete(item)
setIsDeleteConfirmOpen(true)
}}
title="Xóa Note"
width={20}
height={20}
/>
</Table.Td>
</Table.Tr>
)
})}
</Table.Tbody>
</Table>
{/* Confirm Change Status Note Modal */}
<Modal
opened={isStatusConfirmOpen}
onClose={() => {
setIsStatusConfirmOpen(false)
setNoteStatus({
id: 0,
reason: '',
})
}}
centered
size="sm"
classNames={{
content: classes.deleteModal,
}}
>
<Text className={classes.deleteModalTitle} c="dark">
Confirm Change Note Status
</Text>
{noteStatus.reason === 'ONLEAVE' ? (
<Text className={classes.deleteModalContent}>
This action will change the note status from{' '}
<Text span c="red" fw="bold">
Không phép
</Text>{' '}
to{' '}
<Text span c="teal" fw="bold">
Nghỉ phép
</Text>
</Text>
) : (
<Text className={classes.deleteModalContent}>
This action will change the note status from{' '}
<Text span c="teal" fw="bold">
Nghỉ phép
</Text>{' '}
to{' '}
<Text span c="red" fw="bold">
Không phép
</Text>
</Text>
)}
<Text className={classes.deleteModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.deleteModalFooter}>
<Button
variant="outline"
onClick={() => {
setIsStatusConfirmOpen(false)
setNoteStatus({
id: 0,
reason: '',
})
}}
disabled={isDisableStatusBtn}
>
Cancel
</Button>
<Button
className={classes.deleteButton}
onClick={updateNote}
disabled={isDisableStatusBtn}
bg={noteStatus.reason === 'ONLEAVE' ? 'teal' : 'red'}
>
Confirm
</Button>
</Box>
</Modal>
{/* Confirm Delete Note Modal */}
<Modal
opened={isDeleteConfirmOpen}
onClose={() => {
setIsDeleteConfirmOpen(false)
setNoteToDelete(null)
}}
centered
size="sm"
classNames={{
content: classes.deleteModal,
}}
>
<Text className={classes.deleteModalTitle}>Confirm Delete</Text>
<Text className={classes.deleteModalContent}>
This action will change the ticket status to{' '}
<strong>Refused</strong> and delete all related notes.
</Text>
<Text className={classes.deleteModalContent}>
Are you sure you want to proceed?
</Text>
<Box className={classes.deleteModalFooter}>
<Button
variant="outline"
onClick={() => {
setIsDeleteConfirmOpen(false)
setNoteToDelete(null)
}}
disabled={isDisableDeleteBtn}
>
Cancel
</Button>
<Button
className={classes.deleteButton}
onClick={handleDeleteNote}
disabled={isDisableDeleteBtn}
>
Delete
</Button>
</Box>
</Modal>
</Modal>
</div>
)
}

View File

@ -498,7 +498,7 @@ const TicketsManagement = () => {
setDisableBtn(true)
if (action === 'update') {
if (values.status === 'REFUSED') {
if (values.status === 'REFUSED' && item.status !== 'REFUSED') {
setIsRefuseConfirmOpen(true)
} else {
await handleUpdate(values)