From d064b242e6639375c5cd193895450e621fcebeb8 Mon Sep 17 00:00:00 2001 From: dbdbd9 Date: Tue, 1 Jul 2025 13:40:48 +0700 Subject: [PATCH] add update note status --- .../Controllers/LeaveManagementController.php | 24 ++ .../Controllers/TimekeepingController.php | 84 +++-- BACKEND/Modules/Admin/routes/api.php | 1 + .../app/Http/Controllers/UserController.php | 3 + FRONTEND/src/api/Admin.ts | 2 + .../LeaveManagement.module.css | 34 ++ .../pages/LeaveManagement/LeaveManagement.tsx | 350 +++++++++++++++++- .../TicketsManagement/TicketsManagement.tsx | 2 +- 8 files changed, 459 insertions(+), 41 deletions(-) diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index a8b1863..6e122bc 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -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); diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php index f0a046c..03a6b99 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php @@ -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]); } diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index fe3832e..c555911 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -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([ diff --git a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php index ff345da..565b368 100755 --- a/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php +++ b/BACKEND/Modules/Auth/app/Http/Controllers/UserController.php @@ -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'); diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index 54a03d7..1de63ba 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -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' diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.module.css b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.module.css index 10aba71..4a23b38 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.module.css +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.module.css @@ -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; +} \ No newline at end of file diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 86b543e..744229d 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -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({ + 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(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 = () => { {/* Off */} - + { + openDetailOff() + setDetailOffItem(user) + }} + > {totalDayOff > 0 ? ( { + + { + 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={ + + + Chi tiết nghỉ phép {date.year} + + + Nhân viên:{' '} + + {detailOffItem.user.name} + + + + } + size="70%" + > + + + + Time + Status + Action + + + + + {detailOffItem.monthlyLeaveDays + .slice() + .reverse() + .map((item: MonthlyLeaveDays) => { + return ( + + + {item.time_type_name} ({item.day}/{item.month}) + + + + {item.reason_name} + + + + {item.reason_code === 'ONLEAVE' ? ( + { + 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' ? ( + { + setIsStatusConfirmOpen(true) + setNoteStatus({ + id: item.id, + reason: 'ONLEAVE', + }) + }} + title="Chuyển thành có phép" + width={20} + height={20} + /> + ) : ( + '' + )} + + { + setNoteToDelete(item) + setIsDeleteConfirmOpen(true) + }} + title="Xóa Note" + width={20} + height={20} + /> + + + ) + })} + +
+ + {/* Confirm Change Status Note Modal */} + { + setIsStatusConfirmOpen(false) + setNoteStatus({ + id: 0, + reason: '', + }) + }} + centered + size="sm" + classNames={{ + content: classes.deleteModal, + }} + > + + Confirm Change Note Status + + + {noteStatus.reason === 'ONLEAVE' ? ( + + This action will change the note status from{' '} + + Không phép + {' '} + to{' '} + + Nghỉ phép + + + ) : ( + + This action will change the note status from{' '} + + Nghỉ phép + {' '} + to{' '} + + Không phép + + + )} + + + Are you sure you want to proceed? + + + + + + + + {/* Confirm Delete Note Modal */} + { + setIsDeleteConfirmOpen(false) + setNoteToDelete(null) + }} + centered + size="sm" + classNames={{ + content: classes.deleteModal, + }} + > + Confirm Delete + + This action will change the ticket status to{' '} + Refused and delete all related notes. + + + Are you sure you want to proceed? + + + + + + +
) } diff --git a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx index 72f18da..a874105 100755 --- a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx +++ b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx @@ -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) -- 2.39.2