Merge pull request 'update UI leave management, add func update old data' (#127) from vi.ticket-email into dev

Reviewed-on: #127
This commit is contained in:
joseph 2025-06-27 12:51:16 +10:00
commit 543b1af58c
6 changed files with 440 additions and 209 deletions

View File

@ -553,11 +553,20 @@ class TicketController extends Controller
// Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
else if ($remainingOnleaveDaysInMonth < $monthData['days_requested']) {
// Vượt limit
if ($onleaveDaysInMonth >= $maxDaysPerMonth) {
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
$hasInsufficientDays = true;
$month_data_status = 'exceed_max_days';
$onleave_days_will_use = $maxDaysPerMonth - $onleaveDaysInMonth;
$nopay_days_will_use = $monthData['days_requested'] - $maxDaysPerMonth + $onleaveDaysInMonth;
// Phép còn lại > limit
if ($remainingOnleaveDaysInMonth > $maxDaysPerMonth) {
$onleave_days_will_use = $maxDaysPerMonth - $onleaveDaysInMonth;
$nopay_days_will_use = $monthData['days_requested'] - $maxDaysPerMonth + $onleaveDaysInMonth;
}
// Phép còn lại < limit
else {
$onleave_days_will_use = $remainingOnleaveDaysInMonth;
$nopay_days_will_use = $monthData['days_requested'] - $remainingOnleaveDaysInMonth;
}
Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---", [
"Phep" => $onleave_days_will_use,
@ -1409,4 +1418,147 @@ class TicketController extends Controller
return $totalDays;
}
public function updateOldData(int $month, int $year)
{
LeaveDays::where('ld_year', $year)
->update(['ld_day_total' => $month]);
$users = Admin::all();
foreach ($users as $user) {
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $year)
->first();
$notes = Notes::where('n_user_id', $user->id)
->where('n_year', $year)
->where('n_reason', 'ONLEAVE')
->orderBy('n_month')
->orderBy('n_day')
->get()
->groupBy('n_month');
$onleaveDaysTotal = $leaveDay->ld_additional_day;
$previousYearData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $year - 1)
->first();
$ld_additional_day = 0;
$ld_note = '';
if ($previousYearData) {
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day;
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $year - 1)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
if ($ld_additional_day < 0) {
$ld_additional_day = 0;
}
}
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
for ($i = 1; $i <= $month; $i++) {
// Giả lập cộng phép
$onleaveDaysTotal++;
$ld_note = $ld_note . "Cộng phép tháng " . $i . ".\n";
// $tmpOnleaveDaysTotal = $onleaveDaysTotal;
$onleaveDaysInMonth = 0;
$nopayDaysInMonth = 0;
if ($notes->has($i)) {
foreach ($notes[$i] as $note) {
$onleaveDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
}
if ($onleaveDaysInMonth > $onleaveDaysTotal) {
$nopayDaysInMonth = $onleaveDaysInMonth - $onleaveDaysTotal;
$onleaveDaysTotal = 0;
} else {
$onleaveDaysTotal -= $onleaveDaysInMonth;
}
// Xử lý cập nhật lại các note có phép thành không phép
if ($nopayDaysInMonth > 0) {
$revertNotes = $notes->get($i, collect())->reverse();
$nopayDaysUpdated = 0;
foreach ($revertNotes as $note) {
if ($note->n_time_type == 'ALL') {
if ($nopayDaysInMonth - $nopayDaysUpdated == 0.5) {
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'S',
'n_reason' => 'ONLEAVE',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'C',
'n_reason' => 'LEAVE_WITHOUT_PAY',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
$note->delete();
break;
}
$nopayDaysUpdated += 1.0;
$note->update([
'n_reason' => "LEAVE_WITHOUT_PAY"
]);
} else {
$nopayDaysUpdated += 0.5;
$note->update([
'n_reason' => "LEAVE_WITHOUT_PAY"
]);
}
if ($nopayDaysUpdated >= $nopayDaysInMonth) {
break;
}
}
}
}
// Log thông kê sau mỗi tháng
// Log::debug(
// "📊 Thống kê ngày phép Tháng {$i}:\n" .
// " - Tổng phép đầu tháng: $tmpOnleaveDaysTotal\n" .
// " - Có phép: $onleaveDaysInMonth\n" .
// " - Không phép: $nopayDaysInMonth\n" .
// " - Tổng phép cuối tháng: $onleaveDaysTotal\n"
// );
}
$leaveDay->ld_note = $ld_note;
$leaveDay->save();
}
}
}

View File

@ -67,7 +67,7 @@ class AddMonthlyLeaveDays implements ShouldQueue
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
// Xử lý ghi chú
$newNote = "Cập nhật ngày phép đến tháng " . $this->month;
$newNote = "Cộng phép tháng " . $leaveDay->ld_day_total . ".\n";
if (!empty($leaveDay->ld_note)) {
// Nếu đã có ghi chú, thêm ghi chú mới vào và xuống dòng
$leaveDay->ld_note = $leaveDay->ld_note . "\n" . $newNote;
@ -86,7 +86,7 @@ class AddMonthlyLeaveDays implements ShouldQueue
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
// Xử lý ghi chú
$newNote = "Cập nhật ngày phép đến tháng " . $this->month;
$newNote = "Cộng phép tháng " . $leaveDay->ld_day_total . ".\n";
if (!empty($leaveDay->ld_note)) {
// Nếu đã có ghi chú, thêm ghi chú mới vào và xuống dòng
$leaveDay->ld_note = $leaveDay->ld_note . "\n" . $newNote;

View File

@ -52,12 +52,16 @@ class DeductLeaveDays implements ShouldQueue
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
if($usedOnleaveDaysTotal) {
$existingData->ld_additional_day = $existingData->ld_additional_day >= $usedOnleaveDaysTotal ? $usedOnleaveDaysTotal : $existingData->ld_additional_day;
if ($usedOnleaveDaysTotal) {
if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
$ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
$existingData->ld_note = $existingData->ld_note . "\n" . $ld_note;
$existingData->ld_additional_day = $usedOnleaveDaysTotal;
}
} else {
$existingData->ld_additional_day = 0;
}
$existingData->save();
}
}

View File

@ -77,7 +77,10 @@ class InitializeLeaveDays implements ShouldQueue
$ld_additional_day = 0;
}
}
$ld_note = 'Cộng dồn ngày phép năm cũ';
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
// Tạo dữ liệu cho năm hiện tại

View File

@ -0,0 +1,13 @@
<?php
use Carbon\Carbon;
require_once __DIR__ . "/../vendor/autoload.php";
$app = include_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
// Cập nhật lại data cho tới tháng hiện tại
$currentMonth = Carbon::now()->month;
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year

View File

@ -1,19 +1,18 @@
import {
getLeaveManagement,
updateNoteLeave,
exportLeaveManagement,
// getListMaster,
} from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { get, exportFile } from '@/rtk/helpers/apiService'
import { useEffect, useState } from 'react'
import moment from 'moment'
import {
Avatar,
Badge,
Box,
Button,
Drawer,
Flex,
Group,
HoverCard,
Menu,
Select,
Stack,
Table,
Text,
Textarea,
@ -22,13 +21,17 @@ import {
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { IconEdit, IconFileExcel } from '@tabler/icons-react'
import classes from './LeaveManagement.module.css'
import {
getLeaveManagement,
updateNoteLeave,
exportLeaveManagement,
} from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { get, exportFile } from '@/rtk/helpers/apiService'
interface User {
id: number
name: string
@ -265,8 +268,6 @@ const LeaveManagement = () => {
})
}
// console.log(customAddNotes, 'customAddNotes')
const getDetailLeaveDay = (monthlyLeaveDays: MonthlyLeaveDays[]) => {
type MonthlyLeaveDaysAcc = {
[key: string]: { n_user_id: number; month: number; leave_days: number }
@ -290,29 +291,87 @@ const LeaveManagement = () => {
}
const showAllOff = (monthlyLeaveDays: MonthlyLeaveDays[]) => {
let lastmonth = 0
return monthlyLeaveDays.map((itemDay, indexDay) => {
const isNewMonth = lastmonth !== itemDay.month
if (isNewMonth) {
lastmonth = itemDay.month
return monthInYear.map((d, i) => {
let totalOnLeaveMonth = 0
let totalLeaveWithoutPayMonth = 0
monthlyLeaveDays
.filter((item) => item.month === d.value)
.map((item) => {
if (item.reason_code === 'ONLEAVE') {
totalOnLeaveMonth += Number(item.leave_days)
} else {
totalLeaveWithoutPayMonth += Number(item.leave_days)
}
})
if (totalOnLeaveMonth === 0 && totalLeaveWithoutPayMonth === 0) {
return ''
}
return (
<div key={indexDay}>
{isNewMonth && <p>Month {lastmonth}</p>}
<p style={{ paddingLeft: '20px' }}>
- {itemDay.reason_name} ({itemDay.time_type_name}) {itemDay.day}/
{itemDay.month}
</p>
</div>
<Box key={i} px="xs" my="xs">
<Group gap="xs">
{totalOnLeaveMonth > 0 && (
<Badge color="teal" variant="light">
{totalOnLeaveMonth} phép
</Badge>
)}
{totalLeaveWithoutPayMonth > 0 && (
<Badge color="red" variant="light">
{totalLeaveWithoutPayMonth} không phép
</Badge>
)}
<Text size="xs" color="dimmed">
({d.value}/2025)
</Text>
</Group>
</Box>
)
})
}
const showAllTotal = (
ld_day_total: number,
ld_additional_day: number,
ld_special_leave_day: number,
): JSX.Element => {
const showItem = (label: string, value: number, color: string = 'gray') => {
if (value === 0) return null
return (
<Group justify="space-between" gap="xs">
<Text size="sm" c="dimmed">
{label}
</Text>
<Text size="sm" fw={500} c={color}>
{value}
</Text>
</Group>
)
}
return (
<Box p="sm">
<Stack gap={4}>
{showItem(
'Tổng phép hiện có:',
ld_day_total + ld_additional_day + ld_special_leave_day,
'white',
)}
{showItem('+ Phép được cấp năm nay:', ld_day_total, 'teal')}
{showItem('+ Phép tồn năm trước:', ld_additional_day, 'violet')}
{showItem('+ Phép đặc biệt:', ld_special_leave_day, 'orange')}
</Stack>
</Box>
)
}
const handleExport = async () => {
try {
const timestamp = moment().format('DDMMYYYY_HHmmss')
const fileName = `LeaveManagement_${date.year}_${timestamp}.xlsx`
await exportFile(
exportLeaveManagement,
{
@ -335,6 +394,7 @@ const LeaveManagement = () => {
<div className={classes.title}>
<h3>Leave Management</h3>
</div>
{/* Update Leave Day */}
<Drawer
opened={opened1}
onClose={close1}
@ -429,6 +489,7 @@ const LeaveManagement = () => {
onChange={(e) => {
setCustomAddNotes({ ...customAddNotes, note: e.target.value })
}}
rows={10}
/>
<Button
@ -464,65 +525,54 @@ const LeaveManagement = () => {
Save
</Button>
</Drawer>
<Box display={'flex'}>
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
<Box w="100%" display={'flex'}>
<Select
w="50%"
value={date.year}
size="xs"
ml={'sm'}
label="Year"
data={Array.from({ length: 10 }, (_, index) => {
return {
value: (
parseInt(moment(Date.now()).format('YYYY')) -
3 +
index
).toString(),
label: (
parseInt(moment(Date.now()).format('YYYY')) -
3 +
index
).toString(),
disabled:
parseInt(moment(Date.now()).format('YYYY')) - 3 + index >
parseInt(moment(Date.now()).format('YYYY')),
}
})}
onChange={(e) => {
setDate({ ...date, year: e! })
}}
></Select>
</Box>
</Box>
<Box
w="70%"
pl={200}
style={{
display: 'flex',
justifyContent: 'end',
{/* Filter Year, Export Btn */}
<Flex justify="space-between" align="flex-end">
<Select
value={date.year}
size="xs"
label="Year"
data={Array.from({ length: 10 }, (_, index) => {
return {
value: (
parseInt(moment(Date.now()).format('YYYY')) -
3 +
index
).toString(),
label: (
parseInt(moment(Date.now()).format('YYYY')) -
3 +
index
).toString(),
disabled:
parseInt(moment(Date.now()).format('YYYY')) - 3 + index >
parseInt(moment(Date.now()).format('YYYY')),
}
})}
onChange={(e) => {
setDate({ ...date, year: e! })
}}
w={200}
/>
<Button
size="xs"
onClick={handleExport}
leftSection={<IconFileExcel size={16} />}
>
<Box display={'flex'} style={{ alignItems: 'end' }}>
<Button
size="xs"
ml={'sm'}
onClick={handleExport}
leftSection={<IconFileExcel size={16} />}
>
Export Excel
</Button>
</Box>
</Box>
</Box>
<Box>
Export Excel
</Button>
</Flex>
{/* Leave Day Table */}
<Box style={{ overflowX: 'auto' }}>
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
mt={'md'}
miw={1580}
>
<Table.Thead>
<Table.Tr bg={'#228be66b'}>
@ -539,7 +589,9 @@ const LeaveManagement = () => {
style={{
cursor: 'pointer',
width: '40px',
backgroundColor: isCurrentMonth ? '#ffe066' : undefined,
backgroundColor: isCurrentMonth
? '#F2E891'
: undefined,
color: isCurrentMonth ? '#000' : undefined,
fontWeight: isCurrentMonth ? 'bold' : undefined,
}}
@ -550,7 +602,7 @@ const LeaveManagement = () => {
</Menu>
)
})}
<Table.Th ta={'center'} style={{ width: '150px' }}>
<Table.Th ta={'center'} style={{ width: '80px' }}>
Total
</Table.Th>
<Table.Th ta={'center'} style={{ width: '130px' }}>
@ -588,11 +640,11 @@ const LeaveManagement = () => {
mr={'md'}
src={
import.meta.env.VITE_BACKEND_URL.includes('local')
? import.meta.env.VITE_BACKEND_URL +
'storage/' +
user.user.avatar
: import.meta.env.VITE_BACKEND_URL +
'image/storage/' +
? import.meta.env.VITE_BACKEND_URL +
'storage/' +
user.user.avatar
: import.meta.env.VITE_BACKEND_URL +
'image/storage/' +
user.user.avatar
}
/>
@ -601,9 +653,8 @@ const LeaveManagement = () => {
</Tooltip>
</Table.Td>
{/* On leave per month */}
{monthInYear.map((d, i) => {
// const isCurrentMonth =
// Number(date.year) === currentYear && d.value === currentMonth
let leaveDataByMonth = getDetailLeaveDay(
user.monthlyLeaveDays,
)
@ -612,17 +663,43 @@ const LeaveManagement = () => {
let total = monthData ? monthData.leave_days : 0
totalDayOff = totalDayOff + total
let onleaveDaysInMonth: MonthlyLeaveDays[] = []
let nopayDaysInMonth: MonthlyLeaveDays[] = []
let totalOnLeaveMonth = 0
let totalLeaveWithoutPayMonth = 0
let usedAdditionalDay = 0
user.monthlyLeaveDays
.filter((item) => item.month === d.value)
.map((item) => {
if (item.reason_code === 'ONLEAVE') {
totalOnLeave = totalOnLeave + Number(item.leave_days)
totalOnLeaveMonth += Number(item.leave_days)
onleaveDaysInMonth.push(item)
} else {
totalLeaveWithoutPay =
totalLeaveWithoutPay + Number(item.leave_days)
totalLeaveWithoutPayMonth += Number(item.leave_days)
nopayDaysInMonth.push(item)
}
})
// Xử lý hiện thị phép tồn sử dụng
let tmpTotalOnleave = totalOnLeave
totalOnLeave += totalOnLeaveMonth
if (d.value < 4) {
if (totalOnLeave < ld_additional_day) {
usedAdditionalDay = totalOnLeaveMonth
totalOnLeaveMonth = 0
} else {
usedAdditionalDay = ld_additional_day - tmpTotalOnleave
if (usedAdditionalDay >= 0) {
totalOnLeaveMonth -= usedAdditionalDay
}
}
}
totalLeaveWithoutPay += totalLeaveWithoutPayMonth
return (
<Table.Td
bg={total > 0 ? '#ffb5b5' : ''}
@ -631,17 +708,57 @@ const LeaveManagement = () => {
>
<Tooltip
multiline
label={user.monthlyLeaveDays
.filter((item) => item.month === d.value)
.map((itemDay, indexDay) => {
return (
<p key={indexDay}>
- {itemDay.reason_name} (
{itemDay.time_type_name}) {itemDay.day}/
{itemDay.month}
</p>
)
})}
label={
<Box p={4}>
{usedAdditionalDay > 0 && (
<Text fw={500} c="violet" size="sm" mb={4}>
Phép tồn: {usedAdditionalDay}
</Text>
)}
{totalOnLeaveMonth > 0 && (
<Box>
<Text fw={500} c="teal" size="sm" mb={4}>
phép: {totalOnLeaveMonth}
</Text>
</Box>
)}
{totalOnLeaveMonth > 0 ||
usedAdditionalDay > 0 ? (
<Stack gap={2} pl="md">
{onleaveDaysInMonth?.map(
(itemDay: any, indexDay: number) => (
<Text size="xs" key={indexDay}>
{itemDay.time_type_name} (
{itemDay.day}/{itemDay.month})
</Text>
),
)}
</Stack>
) : (
''
)}
{totalLeaveWithoutPayMonth > 0 && (
<Box mt={6}>
<Text fw={500} c="red" size="sm" mb={4}>
Không phép: {totalLeaveWithoutPayMonth}
</Text>
<Stack gap={2} pl="md">
{nopayDaysInMonth?.map(
(itemDay: any, indexDay: number) => (
<Text size="xs" key={indexDay}>
{itemDay.time_type_name} (
{itemDay.day}/{itemDay.month})
</Text>
),
)}
</Stack>
</Box>
)}
</Box>
}
>
<p>{total === 0 ? '' : total}</p>
</Tooltip>
@ -650,128 +767,68 @@ const LeaveManagement = () => {
})}
{/* Total */}
<Table.Td
ta={'center'}
// bg={totalDayLeave > 0 ? '#92e6f2' : ''}
>
<p
style={{
// backgroundColor: '#c3ffc3',
display: ld_day_total > 0 ? 'block' : 'none',
}}
<Table.Td ta={'center'} bg="#92e6f2">
<Tooltip
multiline
label={showAllTotal(
ld_day_total,
ld_additional_day,
ld_special_leave_day,
)}
>
{'Phép năm:'}{' '}
<span
style={{
backgroundColor: '#c3ffc3',
padding: '5px',
borderRadius: '5px',
fontWeight: 'bold',
color: 'black',
}}
>
{ld_day_total}
</span>
</p>
<p
style={{
// backgroundColor: '#92e6f2',
display: ld_additional_day > 0 ? 'block' : 'none',
}}
>
{'Phép năm cũ:'}{' '}
<span
style={{
backgroundColor: '#92e6f2',
padding: '5px',
borderRadius: '5px',
fontWeight: 'bold',
color: 'black',
}}
>
{ld_additional_day}
</span>
</p>
<p
style={{
display: ld_special_leave_day > 0 ? 'block' : 'none',
}}
>
{'Phép đặc biệt:'}{' '}
<span
style={{
backgroundColor: '#b5cafb',
padding: '5px',
borderRadius: '5px',
fontWeight: 'bold',
color: 'black',
}}
>
{ld_special_leave_day}
</span>
</p>
<Text size="sm">{totalDayLeave}</Text>
</Tooltip>
</Table.Td>
{/* Off */}
<Table.Td ta={'center'}>
<Table.Td>
{totalDayOff > 0 ? (
<Tooltip
multiline
label={showAllOff(user.monthlyLeaveDays)}
>
<div>
<p
// style={{ backgroundColor: '#c3ffc3' }}
>
{'Nghỉ phép:'}{' '}
<span
style={{
fontWeight: 'bold',
color: 'black',
backgroundColor: '#c3ffc3',
padding: '5px',
borderRadius: '5px',
}}
<Box>
<Flex justify="space-between" mb="xs" align="center">
<Text size="sm"> phép: </Text>
<Text
size="sm"
bg="#c3ffc3"
fw="bold"
p={5}
style={{ borderRadius: 5 }}
>
{totalOnLeave}
</span>
</p>
<p
// style={{ backgroundColor: '#ffb5b5' }}
>
{'Không phép:'}{' '}
<span
style={{
fontWeight: 'bold',
color: 'black',
backgroundColor: '#ffb5b5',
padding: '5px',
borderRadius: '5px',
}}
</Text>
</Flex>
<Flex justify="space-between" align="center">
<Text size="sm">Không phép: </Text>
<Text
size="sm"
bg="#ffb5b5"
fw="bold"
p={5}
style={{ borderRadius: 5 }}
>
{totalLeaveWithoutPay}
</span>
</p>
</div>
</Text>
</Flex>
</Box>
</Tooltip>
) : (
<></>
''
)}
</Table.Td>
{/* Remaining */}
<Table.Td
ta={'center'}
bg={
totalDayLeave - totalOnLeave == 0
? ''
: totalDayLeave - totalOnLeave > 0
? '#c3ffc3'
: '#ffb5b5'
}
bg={totalDayLeave - totalOnLeave > 0 ? '#b5cafb' : ''}
>
{totalDayLeave - totalOnLeave}
<Text size="sm">{totalDayLeave - totalOnLeave}</Text>
</Table.Td>
{/* Note */}
<Table.Td>
<Box
style={{
@ -800,6 +857,8 @@ const LeaveManagement = () => {
</HoverCard>
</Box>
</Table.Td>
{/* Action */}
<Table.Td ta={'center'}>
<IconEdit
color="green"