update delete note, add update ticket

This commit is contained in:
dbdbd9 2025-06-21 11:11:02 +07:00
parent 3263bce784
commit c2c9322e7d
9 changed files with 344 additions and 89 deletions

View File

@ -379,6 +379,34 @@ class TicketController extends Controller
return response()->json(['data' => $ticket, 'status' => true]); 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ố ngày phép của người dùng. * Kiểm tra số ngày phép của người dùng.
* *
@ -911,7 +939,8 @@ class TicketController extends Controller
'n_year' => $year, 'n_year' => $year,
'n_time_type' => 'S', 'n_time_type' => 'S',
'n_reason' => $onleave, 'n_reason' => $onleave,
'n_note' => $ticket->reason 'n_note' => $ticket->reason,
'ticket_id' => $ticket->id
]); ]);
Notes::create([ Notes::create([
'n_user_id' => $ticket->user_id, 'n_user_id' => $ticket->user_id,
@ -920,7 +949,8 @@ class TicketController extends Controller
'n_year' => $year, 'n_year' => $year,
'n_time_type' => 'C', 'n_time_type' => 'C',
'n_reason' => $leaveWithoutPay, 'n_reason' => $leaveWithoutPay,
'n_note' => $ticket->reason 'n_note' => $ticket->reason,
'ticket_id' => $ticket->id
]); ]);
$daysWillUse = 0; $daysWillUse = 0;
$daysWillUseWithoutPay -= 0.5; $daysWillUseWithoutPay -= 0.5;
@ -934,7 +964,8 @@ class TicketController extends Controller
'n_year' => $year, 'n_year' => $year,
'n_time_type' => $period, 'n_time_type' => $period,
'n_reason' => $onleave, 'n_reason' => $onleave,
'n_note' => $ticket->reason 'n_note' => $ticket->reason,
'ticket_id' => $ticket->id
]); ]);
$daysWillUse -= $use; $daysWillUse -= $use;
} elseif ($daysWillUseWithoutPay > 0) { } elseif ($daysWillUseWithoutPay > 0) {
@ -947,7 +978,8 @@ class TicketController extends Controller
'n_year' => $year, 'n_year' => $year,
'n_time_type' => $period, 'n_time_type' => $period,
'n_reason' => $leaveWithoutPay, 'n_reason' => $leaveWithoutPay,
'n_note' => $ticket->reason 'n_note' => $ticket->reason,
'ticket_id' => $ticket->id
]); ]);
$daysWillUseWithoutPay -= $use; $daysWillUseWithoutPay -= $use;
} }
@ -966,7 +998,8 @@ class TicketController extends Controller
'n_year' => $year, 'n_year' => $year,
'n_time_type' => $result['period'], 'n_time_type' => $result['period'],
'n_reason' => $onleave, // có phép '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_year' => $year,
'n_time_type' => $result['period'], 'n_time_type' => $result['period'],
'n_reason' => $ticket->type, 'n_reason' => $ticket->type,
'n_note' => $ticket->reason 'n_note' => $ticket->reason,
'ticket_id' => $ticket->id
]); ]);

View File

@ -17,6 +17,7 @@ use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking; use Modules\Admin\app\Models\Tracking;
use Maatwebsite\Excel\Facades\Excel; use Maatwebsite\Excel\Facades\Excel;
use App\Exports\TimekeepingExport; use App\Exports\TimekeepingExport;
use Modules\Admin\app\Models\Ticket;
class TimekeepingController extends Controller class TimekeepingController extends Controller
{ {
@ -177,78 +178,92 @@ class TimekeepingController extends Controller
$year = $request->year; $year = $request->year;
$note = Notes::find($id); $note = Notes::find($id);
if ($note) { if (!$note) {
$n_month = $note->n_month; return response()->json(['message' => 'Note not found', 'status' => false]);
$n_year = $note->n_year; }
if ($note->n_reason == "ONLEAVE") { $ticket = Ticket::find($note->ticket_id);
// Get note reason ONLEAVE by $n_month, $n_year not include $note->id & include $note->n_user_id if (!$ticket) {
// $onleave = Notes::getNotesByMonthAndYearAndUserId($n_month, $n_year, $note->n_user_id, $note->id); return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
}
// Get note reason LEAVE_WITHOUT_PAY by $n_month, $n_year & include $note->n_user_id $admin = auth('admins')->user();
$leaveWithoutPay = Notes::getNotesByMonthAndYearAndUserIdAndReason($n_month, $n_year, $note->n_user_id, 'LEAVE_WITHOUT_PAY'); $ticket->updated_by = $admin->name;
$ticket->status = "REFUSED";
$ticket->save();
if (count($leaveWithoutPay) > 0) { Notes::where('ticket_id', $ticket->id)->delete();
$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); $this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]); return response()->json(['message' => 'Delete success', 'status' => true]);
}
return response()->json(['message' => 'Delete fail', 'status' => false]); // $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) public function export(Request $request)

View File

@ -164,6 +164,7 @@ Route::middleware('api')
], function () { ], function () {
Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr'); Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff'); 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::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
Route::get('/delete', [TicketController::class, 'deleteTicket'])->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'); Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');

View File

@ -18,6 +18,7 @@ class Notes extends Model
'n_time_type', 'n_time_type',
'n_reason', 'n_reason',
'n_note', 'n_note',
'ticket_id'
]; ];
/** /**

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -48,6 +48,7 @@ export const getTickets = API_URL + 'v1/admin/ticket/all'
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId' export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
export const deleteTicket = API_URL + 'v1/admin/ticket/delete' export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
export const addTicket = API_URL + 'v1/admin/ticket/create' 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' export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
//Users //Users

View File

@ -46,3 +46,37 @@
.dialogText { .dialogText {
color: light-dark(#2d353c, white); 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 #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

@ -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 { 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 { get } from '@/rtk/helpers/apiService'
import { import {
Badge, Badge,
@ -9,12 +14,13 @@ import {
HoverCard, HoverCard,
Modal, Modal,
Select, Select,
Switch,
Text, Text,
Textarea, Textarea,
} from '@mantine/core' } from '@mantine/core'
import { useForm } from '@mantine/form' import { useForm } from '@mantine/form'
import { notifications } from '@mantine/notifications' 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 moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import classes from './TicketsManagement.module.css' import classes from './TicketsManagement.module.css'
@ -24,6 +30,7 @@ type TTickets = {
ticket_id: number ticket_id: number
admin_note: string admin_note: string
action: string action: string
status: string
} }
type TListTickets = { type TListTickets = {
@ -78,6 +85,7 @@ const TicketsManagement = () => {
end_period: '', end_period: '',
reason: '', reason: '',
type: '', type: '',
status: '',
}) })
const [disableBtn, setDisableBtn] = useState(false) const [disableBtn, setDisableBtn] = useState(false)
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
@ -86,6 +94,7 @@ const TicketsManagement = () => {
}) })
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([]) const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([]) const [dataReason, setDataReason] = useState<DataReason[]>([])
const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState<boolean>(false)
const getListMasterByType = async (type: string) => { const getListMasterByType = async (type: string) => {
try { try {
@ -156,7 +165,7 @@ const TicketsManagement = () => {
}, },
{ {
name: 'reason', name: 'reason',
size: '15%', size: '10%',
header: 'Notes', header: 'Notes',
render: (row: any) => { render: (row: any) => {
return ( return (
@ -198,7 +207,7 @@ const TicketsManagement = () => {
}, },
{ {
name: 'admin_note', name: 'admin_note',
size: '15%', size: '10%',
header: 'Admin Notes', header: 'Admin Notes',
render: (row: any) => { render: (row: any) => {
return ( return (
@ -226,6 +235,18 @@ const TicketsManagement = () => {
size: '10%', size: '10%',
header: 'Updated By', header: 'Updated By',
}, },
{
name: 'updated_at',
size: '10%',
header: 'Updated At',
render: (row: any) => {
if (row?.updated_at) {
return (
<Box>{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
)
}
},
},
{ {
name: '#', name: '#',
size: '5%', size: '5%',
@ -254,7 +275,23 @@ const TicketsManagement = () => {
height={20} height={20}
/> />
</Box> </Box>
) : null ) : (
<Box className={classes.optionIcon}>
<IconEdit
className={classes.editIcon}
onClick={() => {
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}
/>
</Box>
)
}, },
}, },
] ]
@ -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(() => { useEffect(() => {
getAllTickets() getAllTickets()
}, [filter]) }, [filter])
@ -356,6 +414,7 @@ const TicketsManagement = () => {
ticket_id: 0, ticket_id: 0,
action: '', action: '',
admin_note: '', admin_note: '',
status: '',
}, },
}) })
@ -416,21 +475,38 @@ const TicketsManagement = () => {
</Box> </Box>
<Modal <Modal
opened={action === 'confirm' || action === 'refuse'} opened={
action === 'confirm' || action === 'refuse' || action === 'update'
}
onClose={() => { onClose={() => {
setAction('') setAction('')
setIsRefuseConfirmOpen(false)
form.reset() form.reset()
}} }}
title={ title={
<Text pl={'sm'} fw={700} fz={'lg'}> <Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'} {action === 'confirm'
? 'Confirm Ticket'
: action === 'refuse'
? 'Refuse Ticket'
: 'Update Ticket'}
</Text> </Text>
} }
> >
<form <form
onSubmit={form.onSubmit(async (values) => { onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true) setDisableBtn(true)
if (action === 'update') {
if (values.status === 'REFUSED') {
setIsRefuseConfirmOpen(true)
} else {
await handleUpdate(values)
}
} else {
await handleSave(values) await handleSave(values)
}
setDisableBtn(false) setDisableBtn(false)
})} })}
> >
@ -509,14 +585,39 @@ const TicketsManagement = () => {
<Textarea <Textarea
label="Admin Notes" label="Admin Notes"
// required
value={form.values.admin_note} value={form.values.admin_note}
onChange={(e) => form.setFieldValue('admin_note', e.target.value)} onChange={(e) => form.setFieldValue('admin_note', e.target.value)}
/> />
{action === 'update' && item.status !== 'REFUSED' ? (
<Switch
mt="md"
color="red"
label={
<Text size="sm">
Change status to{' '}
<Text span color="red" fw="bold">
Refuse
</Text>
</Text>
}
style={{ width: 'fit-content' }}
checked={form.values.status === 'REFUSED'}
onChange={(event) =>
form.setFieldValue(
'status',
event.currentTarget.checked ? 'REFUSED' : 'CONFIRMED',
)
}
/>
) : (
''
)}
<Box ta={'center'}> <Box ta={'center'}>
<Button <Button
mt={'lg'} mt={'lg'}
bg={'green'} bg={action === 'update' ? 'blue' : 'green'}
type="submit" type="submit"
disabled={disableBtn} disabled={disableBtn}
> >
@ -526,6 +627,44 @@ const TicketsManagement = () => {
</Box> </Box>
</form> </form>
</Modal> </Modal>
<Modal
opened={isRefuseConfirmOpen}
onClose={() => {
setIsRefuseConfirmOpen(false)
}}
centered
size="sm"
classNames={{
content: classes.deleteModal,
}}
>
<Text className={classes.deleteModalTitle}>Confirm Update</Text>
<Text className={classes.deleteModalContent}>
Changing ticket status to <strong>Refused</strong> will also 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={() => {
setIsRefuseConfirmOpen(false)
}}
>
Cancel
</Button>
<Button
className={classes.deleteButton}
onClick={() => handleUpdate(form.values)}
>
Confirm
</Button>
</Box>
</Modal>
</div> </div>
) )
} }

View File

@ -493,7 +493,11 @@ const Timekeeping = () => {
> >
<Text className={classes.deleteModalTitle}>Confirm Delete</Text> <Text className={classes.deleteModalTitle}>Confirm Delete</Text>
<Text className={classes.deleteModalContent}> <Text className={classes.deleteModalContent}>
Are you sure you want to delete this note? 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> </Text>
<Box className={classes.deleteModalFooter}> <Box className={classes.deleteModalFooter}>
<Button <Button