From c2c9322e7d9e9e5ca2d97989f37d0069ecf690d8 Mon Sep 17 00:00:00 2001 From: dbdbd9 Date: Sat, 21 Jun 2025 11:11:02 +0700 Subject: [PATCH] update delete note, add update ticket --- .../app/Http/Controllers/TicketController.php | 46 ++++- .../Controllers/TimekeepingController.php | 155 +++++++++-------- BACKEND/Modules/Admin/routes/api.php | 1 + BACKEND/app/Models/Notes.php | 1 + ...050728_add_ticket_relation_notes_table.php | 26 +++ FRONTEND/src/api/Admin.ts | 1 + .../TicketsManagement.module.css | 36 +++- .../TicketsManagement/TicketsManagement.tsx | 161 ++++++++++++++++-- .../src/pages/Timekeeping/Timekeeping.tsx | 6 +- 9 files changed, 344 insertions(+), 89 deletions(-) create mode 100644 BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php index bc14288..270bae2 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TicketController.php @@ -379,6 +379,34 @@ class TicketController extends Controller return response()->json(['data' => $ticket, 'status' => true]); } + public function updateTicket(Request $request) + { + // Validate input + $rules = [ + 'ticket_id' => 'required|exists:tickets,id', + 'status' => 'required|string|in:CONFIRMED,REFUSED' + ]; + $request->validate($rules); + + $ticket = Ticket::find($request->input('ticket_id')); + if (!$ticket) { + return AbstractController::ResultError("Ticket not found."); + } + + $admin = auth('admins')->user(); + // Delete related note, if status change to Refuse + if ($request->status == "REFUSED") { + $ticket->status = "REFUSED"; + Notes::where('ticket_id', $ticket->id)->delete(); + } + + $ticket->updated_by = $admin->name; + $ticket->admin_note = $request->admin_note; + $ticket->save(); + + return AbstractController::ResultSuccess($ticket, "Ticket updated successfully!"); + } + /** * Kiểm tra số dư ngày phép của người dùng. * @@ -911,7 +939,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => 'S', 'n_reason' => $onleave, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); Notes::create([ 'n_user_id' => $ticket->user_id, @@ -920,7 +949,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => 'C', 'n_reason' => $leaveWithoutPay, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); $daysWillUse = 0; $daysWillUseWithoutPay -= 0.5; @@ -934,7 +964,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => $period, 'n_reason' => $onleave, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); $daysWillUse -= $use; } elseif ($daysWillUseWithoutPay > 0) { @@ -947,7 +978,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => $period, 'n_reason' => $leaveWithoutPay, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); $daysWillUseWithoutPay -= $use; } @@ -966,7 +998,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => $result['period'], 'n_reason' => $onleave, // có phép - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); } } @@ -1018,7 +1051,8 @@ class TicketController extends Controller 'n_year' => $year, 'n_time_type' => $result['period'], 'n_reason' => $ticket->type, - 'n_note' => $ticket->reason + 'n_note' => $ticket->reason, + 'ticket_id' => $ticket->id ]); diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php index b33b165..1b33037 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/TimekeepingController.php @@ -17,6 +17,7 @@ use Modules\Admin\app\Models\MonthlyTimekeeping; use Modules\Admin\app\Models\Tracking; use Maatwebsite\Excel\Facades\Excel; use App\Exports\TimekeepingExport; +use Modules\Admin\app\Models\Ticket; class TimekeepingController extends Controller { @@ -177,78 +178,92 @@ class TimekeepingController extends Controller $year = $request->year; $note = Notes::find($id); - if ($note) { - $n_month = $note->n_month; - $n_year = $note->n_year; - - if ($note->n_reason == "ONLEAVE") { - // Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id - // $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id); - - // Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id - $leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY'); - - if (count($leaveWithoutPay) > 0) { - $deletedValue = ($note->n_time_type === 'ALL') ? 1.0 : 0.5; - $needUpdate = $deletedValue; - // dd($needUpdate, $leaveWithoutPay); - foreach ($leaveWithoutPay as $lwNote) { - if ($needUpdate <= 0) break; - - if ($lwNote->n_time_type === 'ALL') { - if ($needUpdate == 1.0) { - // Chuyển cả note ALL thành phép - $lwNote->update(['n_reason' => 'ONLEAVE']); - $needUpdate = 0; - break; - } else { // $needUpdate == 0.5 - // Tách ALL thành 2 note S và C, chuyển S thành phép, C giữ không phép - Notes::create([ - 'n_user_id' => $lwNote->n_user_id, - 'n_day' => $lwNote->n_day, - 'n_month' => $lwNote->n_month, - 'n_year' => $lwNote->n_year, - 'n_time_type' => 'S', - 'n_reason' => 'ONLEAVE', - 'n_note' => $lwNote->n_note - ]); - Notes::create([ - 'n_user_id' => $lwNote->n_user_id, - 'n_day' => $lwNote->n_day, - 'n_month' => $lwNote->n_month, - 'n_year' => $lwNote->n_year, - 'n_time_type' => 'C', - 'n_reason' => 'LEAVE_WITHOUT_PAY', - 'n_note' => $lwNote->n_note - ]); - $lwNote->delete(); - $needUpdate = 0; - break; - } - } else { - // Nếu $lwNote->n_time_type == 'S' hoặc 'C' => 0.5 - if ($needUpdate == 1.0) { - // Chuyển cả note ALL thành phép - $lwNote->update(['n_reason' => 'ONLEAVE']); - $needUpdate -= 0.5; - } else { // $needUpdate == 0.5 - // S hoặc C, chỉ cần chuyển đúng 0.5 ngày - $lwNote->update(['n_reason' => 'ONLEAVE']); - $needUpdate = 0; - break; - } - } - } - } else { - // Khi note phép và k tồn tại nghỉ không phép => phép + dồn cho tháng sau - } - } - $note->delete(); - $this->createOrUpdateRecordForCurrentMonth($month, $year); - return response()->json(['message' => 'Delete success', 'status' => true]); + if (!$note) { + return response()->json(['message' => 'Note not found', 'status' => false]); } - return response()->json(['message' => 'Delete fail', 'status' => false]); + $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(); + $ticket->updated_by = $admin->name; + $ticket->status = "REFUSED"; + $ticket->save(); + + Notes::where('ticket_id', $ticket->id)->delete(); + $this->createOrUpdateRecordForCurrentMonth($month, $year); + return response()->json(['message' => 'Delete success', 'status' => true]); + + // $n_month = $note->n_month; + // $n_year = $note->n_year; + + // if ($note->n_reason == "ONLEAVE") { + // // Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id + // // $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id); + + // // Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id + // $leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY'); + + // if (count($leaveWithoutPay) > 0) { + // $deletedValue = ($note->n_time_type === 'ALL') ? 1.0 : 0.5; + // $needUpdate = $deletedValue; + // // dd($needUpdate, $leaveWithoutPay); + // foreach ($leaveWithoutPay as $lwNote) { + // if ($needUpdate <= 0) break; + + // if ($lwNote->n_time_type === 'ALL') { + // if ($needUpdate == 1.0) { + // // Chuyển cả note ALL thành phép + // $lwNote->update(['n_reason' => 'ONLEAVE']); + // $needUpdate = 0; + // break; + // } else { // $needUpdate == 0.5 + // // Tách ALL thành 2 note S và C, chuyển S thành phép, C giữ không phép + // Notes::create([ + // 'n_user_id' => $lwNote->n_user_id, + // 'n_day' => $lwNote->n_day, + // 'n_month' => $lwNote->n_month, + // 'n_year' => $lwNote->n_year, + // 'n_time_type' => 'S', + // 'n_reason' => 'ONLEAVE', + // 'n_note' => $lwNote->n_note + // ]); + // Notes::create([ + // 'n_user_id' => $lwNote->n_user_id, + // 'n_day' => $lwNote->n_day, + // 'n_month' => $lwNote->n_month, + // 'n_year' => $lwNote->n_year, + // 'n_time_type' => 'C', + // 'n_reason' => 'LEAVE_WITHOUT_PAY', + // 'n_note' => $lwNote->n_note + // ]); + // $lwNote->delete(); + // $needUpdate = 0; + // break; + // } + // } else { + // // Nếu $lwNote->n_time_type == 'S' hoặc 'C' => 0.5 + // if ($needUpdate == 1.0) { + // // Chuyển cả note ALL thành phép + // $lwNote->update(['n_reason' => 'ONLEAVE']); + // $needUpdate -= 0.5; + // } else { // $needUpdate == 0.5 + // // S hoặc C, chỉ cần chuyển đúng 0.5 ngày + // $lwNote->update(['n_reason' => 'ONLEAVE']); + // $needUpdate = 0; + // break; + // } + // } + // } + // } else { + // // Khi note phép và k tồn tại nghỉ không phép => phép + dồn cho tháng sau + // } + // } + // $note->delete(); + // $this->createOrUpdateRecordForCurrentMonth($month, $year); + // return response()->json(['message' => 'Delete success', 'status' => true]); } public function export(Request $request) diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index ae1b8db..fe3832e 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -164,6 +164,7 @@ Route::middleware('api') ], function () { Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr'); Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff'); + Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr'); Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff'); Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff'); Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin'); diff --git a/BACKEND/app/Models/Notes.php b/BACKEND/app/Models/Notes.php index 659fcda..f1a90a7 100644 --- a/BACKEND/app/Models/Notes.php +++ b/BACKEND/app/Models/Notes.php @@ -18,6 +18,7 @@ class Notes extends Model 'n_time_type', 'n_reason', 'n_note', + 'ticket_id' ]; /** diff --git a/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php b/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php new file mode 100644 index 0000000..1a9b528 --- /dev/null +++ b/BACKEND/database/migrations/2025_06_20_050728_add_ticket_relation_notes_table.php @@ -0,0 +1,26 @@ +foreignId('ticket_id') + ->nullable() + ->constrained('tickets') + ->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::table('notes', function (Blueprint $table) { + $table->dropForeign(['ticket_id']); + $table->dropColumn('ticket_id'); + }); + } +}; diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index b1c613f..54a03d7 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -48,6 +48,7 @@ export const getTickets = API_URL + 'v1/admin/ticket/all' export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId' export const deleteTicket = API_URL + 'v1/admin/ticket/delete' export const addTicket = API_URL + 'v1/admin/ticket/create' +export const updateTicket = API_URL + 'v1/admin/ticket/update' export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket' //Users diff --git a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.module.css b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.module.css index b35dd04..97ccfa2 100755 --- a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.module.css +++ b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.module.css @@ -45,4 +45,38 @@ .dialogText { color: light-dark(#2d353c, white); - } \ No newline at end of file + } + + /* 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/TicketsManagement/TicketsManagement.tsx b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx index e5584dd..f90a428 100755 --- a/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx +++ b/FRONTEND/src/pages/TicketsManagement/TicketsManagement.tsx @@ -1,6 +1,11 @@ -import { getListMaster, getTickets, handleTicket } from '@/api/Admin' +import { + getListMaster, + getTickets, + handleTicket, + updateTicket, +} from '@/api/Admin' import { DataTablePagination } from '@/components/DataTable/DataTable' -import { create } from '@/rtk/helpers/CRUD' +import { create, update } from '@/rtk/helpers/CRUD' import { get } from '@/rtk/helpers/apiService' import { Badge, @@ -9,12 +14,13 @@ import { HoverCard, Modal, Select, + Switch, Text, Textarea, } from '@mantine/core' import { useForm } from '@mantine/form' import { notifications } from '@mantine/notifications' -import { IconCheckbox, IconSquareXFilled } from '@tabler/icons-react' +import { IconCheckbox, IconEdit, IconSquareXFilled } from '@tabler/icons-react' import moment from 'moment' import { useEffect, useState } from 'react' import classes from './TicketsManagement.module.css' @@ -24,6 +30,7 @@ type TTickets = { ticket_id: number admin_note: string action: string + status: string } type TListTickets = { @@ -78,6 +85,7 @@ const TicketsManagement = () => { end_period: '', reason: '', type: '', + status: '', }) const [disableBtn, setDisableBtn] = useState(false) const [filter, setFilter] = useState({ @@ -86,6 +94,7 @@ const TicketsManagement = () => { }) const [dataTimeType, setDataTimeType] = useState([]) const [dataReason, setDataReason] = useState([]) + const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState(false) const getListMasterByType = async (type: string) => { try { @@ -156,7 +165,7 @@ const TicketsManagement = () => { }, { name: 'reason', - size: '15%', + size: '10%', header: 'Notes', render: (row: any) => { return ( @@ -198,7 +207,7 @@ const TicketsManagement = () => { }, { name: 'admin_note', - size: '15%', + size: '10%', header: 'Admin Notes', render: (row: any) => { return ( @@ -226,6 +235,18 @@ const TicketsManagement = () => { size: '10%', header: 'Updated By', }, + { + name: 'updated_at', + size: '10%', + header: 'Updated At', + render: (row: any) => { + if (row?.updated_at) { + return ( + {moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')} + ) + } + }, + }, { name: '#', size: '5%', @@ -254,7 +275,23 @@ const TicketsManagement = () => { height={20} /> - ) : null + ) : ( + + { + setAction('update') + setItem(row) + form.reset() + console.log(row) + form.setFieldValue('status', row.status) + form.setFieldValue('admin_note', row.admin_note) + }} + width={20} + height={20} + /> + + ) }, }, ] @@ -347,6 +384,27 @@ const TicketsManagement = () => { } } + const handleUpdate = async (values: TTickets) => { + try { + const res = await update( + updateTicket + `?ticket_id=${item?.id}`, + { + ticket_id: item.id, + admin_note: values.admin_note, + status: values.status, + }, + getAllTickets, + ) + if (res === true) { + setAction('') + setIsRefuseConfirmOpen(false) + form.reset() + } + } catch (error) { + console.log(error) + } + } + useEffect(() => { getAllTickets() }, [filter]) @@ -356,6 +414,7 @@ const TicketsManagement = () => { ticket_id: 0, action: '', admin_note: '', + status: '', }, }) @@ -416,21 +475,38 @@ const TicketsManagement = () => { { setAction('') + setIsRefuseConfirmOpen(false) form.reset() }} title={ - {action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'} + {action === 'confirm' + ? 'Confirm Ticket' + : action === 'refuse' + ? 'Refuse Ticket' + : 'Update Ticket'} } >
{ setDisableBtn(true) - await handleSave(values) + + if (action === 'update') { + if (values.status === 'REFUSED') { + setIsRefuseConfirmOpen(true) + } else { + await handleUpdate(values) + } + } else { + await handleSave(values) + } + setDisableBtn(false) })} > @@ -509,14 +585,39 @@ const TicketsManagement = () => {