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]);
}
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.
*
@ -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
]);

View File

@ -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)

View File

@ -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');

View File

@ -18,6 +18,7 @@ class Notes extends Model
'n_time_type',
'n_reason',
'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 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

View File

@ -45,4 +45,38 @@
.dialogText {
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 { 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<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([])
const [isRefuseConfirmOpen, setIsRefuseConfirmOpen] = useState<boolean>(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 (
<Box>{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
)
}
},
},
{
name: '#',
size: '5%',
@ -254,7 +275,23 @@ const TicketsManagement = () => {
height={20}
/>
</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(() => {
getAllTickets()
}, [filter])
@ -356,6 +414,7 @@ const TicketsManagement = () => {
ticket_id: 0,
action: '',
admin_note: '',
status: '',
},
})
@ -416,21 +475,38 @@ const TicketsManagement = () => {
</Box>
<Modal
opened={action === 'confirm' || action === 'refuse'}
opened={
action === 'confirm' || action === 'refuse' || action === 'update'
}
onClose={() => {
setAction('')
setIsRefuseConfirmOpen(false)
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'confirm' ? 'Confirm Ticket' : 'Refuse Ticket'}
{action === 'confirm'
? 'Confirm Ticket'
: action === 'refuse'
? 'Refuse Ticket'
: 'Update Ticket'}
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values) => {
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 = () => {
<Textarea
label="Admin Notes"
// required
value={form.values.admin_note}
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'}>
<Button
mt={'lg'}
bg={'green'}
bg={action === 'update' ? 'blue' : 'green'}
type="submit"
disabled={disableBtn}
>
@ -526,6 +627,44 @@ const TicketsManagement = () => {
</Box>
</form>
</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>
)
}

View File

@ -493,7 +493,11 @@ const Timekeeping = () => {
>
<Text className={classes.deleteModalTitle}>Confirm Delete</Text>
<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>
<Box className={classes.deleteModalFooter}>
<Button