Merge pull request 'truong-sprint-1' (#9) from truong-sprint-1 into master

Reviewed-on: #9
This commit is contained in:
joseph 2024-08-09 17:59:18 +10:00
commit 4f492df167
9 changed files with 406 additions and 35 deletions

View File

@ -13,8 +13,10 @@ use Carbon\Carbon;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Category; use Modules\Admin\app\Models\Category;
use Modules\Admin\app\Models\Ticket; use Modules\Admin\app\Models\Ticket;
use Modules\Admin\app\Models\Tracking;
class TicketController extends Controller class TicketController extends Controller
{ {
@ -288,7 +290,7 @@ class TicketController extends Controller
// Update updated_by and admin_note in tickets table // Update updated_by and admin_note in tickets table
// Send notification email to users // Send notification email to users
$user = Admin::find($ticket->user_id);
if ($action == "confirm") { if ($action == "confirm") {
foreach ($results as $result) { foreach ($results as $result) {
list($year, $month, $day) = explode('-', $result['date']); list($year, $month, $day) = explode('-', $result['date']);
@ -301,6 +303,38 @@ class TicketController extends Controller
'n_reason' => $ticket->type, 'n_reason' => $ticket->type,
'n_note' => $ticket->reason 'n_note' => $ticket->reason
]); ]);
if ($ticket->type == "WFH") {
$type = $result['period'];
$date = Carbon::create($year, $month, $day)->setTimezone(env('TIME_ZONE'));
//Default: ALL
$start = $date->copy()->setTime(7, 31, 11);
$end = $date->copy()->setTime(17, 1, 11);
if ($type == 'S') {
$end = $date->copy()->setTime(11, 31, 11);
} else if ($type == 'C') {
$start = $date->copy()->setTime(11, 31, 11);
}
Tracking::insert([
[
'name' => $user->name,
'user_id' => $user->id,
'status' => 'check in',
'time_string' => $start->format('Y-m-d H:i:s'),
'created_at' => $start->setTimezone('UTC')
],
[
'name' => $user->name,
'user_id' => $user->id,
'status' => 'check out',
'time_string' => $end->format('Y-m-d H:i:s'),
'created_at' => $end->setTimezone('UTC')
]
]);
}
} }
$ticket['updated_by'] = $admin->name; $ticket['updated_by'] = $admin->name;

View File

@ -161,4 +161,27 @@ class TimekeepingController extends Controller
return response()->json(['status' => true, 'message' => 'Update successfully']); return response()->json(['status' => true, 'message' => 'Update successfully']);
} }
public function deleteNote(Request $request)
{
$rules = [
'id' => 'required'
];
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$month = $request->month;
$year = $request->year;
$note = Notes::find($id);
if ($note) {
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]);
}
return response()->json(['message' => 'Delete fail', 'status' => false]);
}
} }

View File

@ -111,6 +111,7 @@ Route::middleware('api')
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff'); Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr'); Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr'); Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin'); Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr'); Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
}); });

View File

@ -34,6 +34,7 @@ class Notes extends Model
->where('n_month', $month) ->where('n_month', $month)
->where('n_year', $year) ->where('n_year', $year)
->select( ->select(
'notes.id as n_id',
'n_user_id', 'n_user_id',
'n_day', 'n_day',
'n_month', 'n_month',

View File

@ -72,6 +72,7 @@ trait AnalyzeData
if (count($hasNotes) > 0) { if (count($hasNotes) > 0) {
foreach ($hasNotes as $k_Note => $value_Note) { foreach ($hasNotes as $k_Note => $value_Note) {
$notes[$k_Note] = [ $notes[$k_Note] = [
'id' => $value_Note->n_id,
'timeType' => $value_Note->n_time_type, 'timeType' => $value_Note->n_time_type,
'timeTypeName' => $value_Note->time_type_name, 'timeTypeName' => $value_Note->time_type_name,
'reason' => $value_Note->n_reason, 'reason' => $value_Note->n_reason,
@ -86,6 +87,7 @@ trait AnalyzeData
$notes = []; $notes = [];
foreach ($hasNotes as $k_Note => $value_Note) { foreach ($hasNotes as $k_Note => $value_Note) {
$notes[$k_Note] = [ $notes[$k_Note] = [
'id' => $value_Note->n_id,
'timeType' => $value_Note->n_time_type, 'timeType' => $value_Note->n_time_type,
'timeTypeName' => $value_Note->time_type_name, 'timeTypeName' => $value_Note->time_type_name,
'reason' => $value_Note->n_reason, 'reason' => $value_Note->n_reason,

View File

@ -68,6 +68,8 @@ export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping' export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
export const updateMultipleUserWorkingTime = API_URL + 'v1/admin/timekeeping/addMutilple' export const updateMultipleUserWorkingTime = API_URL + 'v1/admin/timekeeping/addMutilple'
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote' export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
export const deleteNote = API_URL + 'v1/admin/timekeeping/delete'
export const updateCacheMonth = API_URL + 'v1/admin/timekeeping/update-cache-month'
export const updateWorkingDays = API_URL + 'v1/admin/timekeeping/update-working-days' export const updateWorkingDays = API_URL + 'v1/admin/timekeeping/update-working-days'
//Category //Category

View File

@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Code, Code,
Divider,
Group, Group,
Modal, Modal,
PasswordInput, PasswordInput,
@ -38,19 +39,36 @@ import classes from './NavbarSimpleColored.module.css'
const data = [ const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome }, // { link: '/dashboard', label: 'Dashboard', icon: IconHome },
{ link: '/timekeeping', label: 'Timekeeping', icon: IconCalendar }, {
{ link: '/tracking', label: 'Check in/out', icon: IconScan }, link: '/timekeeping',
{ link: '/worklogs', label: 'Worklogs', icon: IconReport }, label: 'Timekeeping',
icon: IconCalendar,
group: 'normal',
},
{
link: '/tracking',
label: 'Check in/out',
icon: IconScan,
group: 'normal',
},
{
link: '/worklogs',
label: 'Worklogs',
icon: IconReport,
group: 'normal',
},
{ {
link: '/leave-management', link: '/leave-management',
label: 'Leave Management', label: 'Leave Management',
icon: IconCalendarClock, icon: IconCalendarClock,
group: 'normal',
}, },
{ link: '/tickets', label: 'Tickets', icon: IconTicket }, { link: '/tickets', label: 'Tickets', icon: IconTicket, group: 'normal' },
{ {
link: '/tickets-management', link: '/tickets-management',
label: 'Tickets Management', label: 'Tickets Management',
icon: IconDevices, icon: IconDevices,
group: 'admin',
}, },
// { link: '/jira', label: 'Jira', icon: IconSubtask }, // { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush }, // { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
@ -106,31 +124,90 @@ const Navbar = ({
getInitialValueInEffect: true, getInitialValueInEffect: true,
}) })
const links = data.map((item) => { // const links = data.map((item) => {
if (user?.user?.permission !== 'admin' && item.link === '/tickets-management') // if (
return null // user?.user?.permission !== 'admin' &&
// item.link === '/tickets-management'
// )
// return null
// return (
// <a
// className={classes.link}
// data-active={item.label === active || undefined}
// key={item.label}
// onClick={() => {
// // setHeader(item.label);
// setActive(active)
// if (active !== item.label) {
// navigate(item.link)
// }
// }}
// >
// <item.icon className={classes.linkIcon} stroke={1.5} />
// <span
// className={`${classes.itemLabel} ${
// isCompactMenu ? classes.labelCompactMenu : ''
// } `}
// >
// {item.label}
// </span>
// </a>
// )
// })
const group = [
{ name: 'normal', label: 'Normal' },
{ name: 'admin', label: 'Admin' },
]
const links = group.map((g) => {
return ( return (
<a <div key={g.name}>
className={classes.link} <Divider
data-active={item.label === active || undefined} display={
key={item.label} g.name === 'normal'
onClick={() => { ? 'block'
// setHeader(item.label); : user?.user?.permission === g.name
setActive(active) ? 'block'
if (active !== item.label) { : 'none'
navigate(item.link)
} }
}} my="xs"
> label={
<item.icon className={classes.linkIcon} stroke={1.5} /> <span style={{ color: 'white', fontWeight: 700 }}>{g.label}</span>
<span }
className={`${classes.itemLabel} ${ labelPosition="center"
isCompactMenu ? classes.labelCompactMenu : '' />
} `} {data
> .filter((i) => {
{item.label} return (
</span> i.group === g.name &&
</a> (user?.user?.permission === 'admin' || g.name !== 'admin')
)
})
.map((item) => (
<a
className={classes.link}
data-active={item.label === active || undefined}
key={item.link}
onClick={() => {
// setHeader(item.label);
setActive(active)
if (active !== item.label) {
navigate(item.link)
}
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span
className={`${classes.itemLabel} ${
isCompactMenu ? classes.labelCompactMenu : ''
} `}
>
{item.label}
</span>
</a>
))}
</div>
) )
}) })

View File

@ -18,6 +18,7 @@ import { IconCheckbox, 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'
import { DateInput } from '@mantine/dates'
type TTickets = { type TTickets = {
ticket_id: number ticket_id: number
@ -52,17 +53,38 @@ interface DataReason {
c_name: string c_name: string
} }
interface DataTimeType {
id: number
c_code: string
c_name: string
}
interface FilterInfo {
key: string
name: string
type: string
}
const TicketsManagement = () => { const TicketsManagement = () => {
const [listTickets, setListTiskets] = useState<TListTickets>({ const [listTickets, setListTiskets] = useState<TListTickets>({
data: [], data: [],
}) })
const [action, setAction] = useState('') const [action, setAction] = useState('')
const [item, setItem] = useState({ id: 0 }) const [item, setItem] = useState({
id: 0,
start_date: '',
start_period: '',
end_date: '',
end_period: '',
reason: '',
type: '',
})
const [disableBtn, setDisableBtn] = useState(false) const [disableBtn, setDisableBtn] = useState(false)
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
typeReason: '', typeReason: '',
statusFilter: '', statusFilter: '',
}) })
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([]) const [dataReason, setDataReason] = useState<DataReason[]>([])
const getListMasterByType = async (type: string) => { const getListMasterByType = async (type: string) => {
@ -86,6 +108,11 @@ const TicketsManagement = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const resultTimeType = await getListMasterByType('TIME_TYPE')
setDataTimeType(
resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
)
const resultReason = await getListMasterByType('REASON') const resultReason = await getListMasterByType('REASON')
setDataReason(resultReason) setDataReason(resultReason)
} }
@ -369,7 +396,7 @@ const TicketsManagement = () => {
label="Status" label="Status"
data={[ data={[
{ value: 'WAITING', label: 'WAITING' }, { value: 'WAITING', label: 'WAITING' },
{ value: 'CONFIRM', label: 'CONFIRM' }, { value: 'CONFIRMED', label: 'CONFIRMED' },
{ value: 'REFUSED', label: 'REFUSED' }, { value: 'REFUSED', label: 'REFUSED' },
]} ]}
onChange={(e) => { onChange={(e) => {
@ -411,6 +438,78 @@ const TicketsManagement = () => {
})} })}
> >
<Box pl={'md'} pr={'md'}> <Box pl={'md'} pr={'md'}>
<Box display={'flex'}>
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'40%'}>
<DateInput
variant="unstyled"
readOnly
mb={'md'}
label={'From Date'}
value={new Date(item.start_date)}
valueFormat="DD/MM/YYYY"
></DateInput>
<Select
style={{ border: 'none' }}
variant="unstyled"
mb={'md'}
label={'Start Period'}
data={dataTimeType.map((item) => {
return { value: item.c_code.toString(), label: item.c_name }
})}
value={item.start_period}
error={form.errors.start_period}
onChange={(e) => form.setFieldValue('start_period', e!)}
/>
<Select
variant="unstyled"
mb={'md'}
searchable
label="Type"
data={dataReason.map((user) => ({
value: user.c_code.toString(),
label: user.c_name,
}))}
value={item.type}
error={form.errors.type}
onChange={(e) => form.setFieldValue('type', e!)}
/>
</Box>
<Box
style={{ display: 'flex', flexFlow: 'column' }}
w={'20%'}
></Box>
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'40%'}>
<DateInput
variant="unstyled"
mb={'md'}
label={'End Date'}
value={new Date(item.end_date)}
valueFormat="DD/MM/YYYY"
onChange={(e) => form.setFieldValue('end_date', e!)}
></DateInput>
<Select
variant="unstyled"
mb={'md'}
label={'End Period'}
data={dataTimeType.map((item) => {
return { value: item.c_code.toString(), label: item.c_name }
})}
value={item.end_period}
error={form.errors.end_period}
onChange={(e) => form.setFieldValue('end_period', e!)}
/>
</Box>
</Box>
<Textarea
label="Reason"
variant="unstyled"
value={item.reason}
onChange={(e) => form.setFieldValue('reason', e.target.value)}
/>
<Textarea <Textarea
label="Admin Notes" label="Admin Notes"
required required

View File

@ -1,11 +1,12 @@
import { import {
deleteNote,
getListMaster, getListMaster,
getTheTimesheet, getTheTimesheet,
updateMultipleUserWorkingTime, updateMultipleUserWorkingTime,
updateNote, updateNote,
updateWorkingDays, updateWorkingDays
} from '@/api/Admin' } from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD' import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService' import { get } from '@/rtk/helpers/apiService'
import { import {
Box, Box,
@ -19,7 +20,7 @@ import {
Text, Text,
Textarea, Textarea,
TextInput, TextInput,
Tooltip, Tooltip
} from '@mantine/core' } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks' import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications' import { notifications } from '@mantine/notifications'
@ -27,7 +28,8 @@ import {
IconCheck, IconCheck,
IconExclamationMark, IconExclamationMark,
IconPointFilled, IconPointFilled,
IconX IconTrash,
IconX,
} from '@tabler/icons-react' } from '@tabler/icons-react'
import moment from 'moment' import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -69,6 +71,7 @@ interface HistoryValue {
} }
interface NoteValue { interface NoteValue {
id: number
note: string note: string
reason: string reason: string
timeType: string timeType: string
@ -113,6 +116,7 @@ const Timekeeping = () => {
reason: string reason: string
note: string note: string
day: number day: number
notes: NoteValue[]
}>({ }>({
user: { user: {
id: 0, id: 0,
@ -122,6 +126,7 @@ const Timekeeping = () => {
reason: '', reason: '',
note: '', note: '',
day: 0, day: 0,
notes: [],
}) })
const [workingDays, setWorkingDays] = useState(30) const [workingDays, setWorkingDays] = useState(30)
@ -175,6 +180,23 @@ const Timekeeping = () => {
setData( setData(
res.data.filter((u: UserData) => u.user.permission.includes('staff')), res.data.filter((u: UserData) => u.user.permission.includes('staff')),
) )
if (
res.data.find(
(item: UserData) => item.user.id === customAddNotes.user.id,
)?.user.id
) {
setCustomAddNotes({
...customAddNotes,
notes: (res.data
.find((item: UserData) => item.user.id === customAddNotes.user.id)
?.history?.find(
(itemHistory: History) =>
itemHistory.day === customAddNotes.day,
)?.notes ?? []) as NoteValue[],
})
}
setDaysInMonth( setDaysInMonth(
Array.from({ length: getDaysInMonth() }, (_, index) => index + 1), Array.from({ length: getDaysInMonth() }, (_, index) => index + 1),
) )
@ -283,6 +305,20 @@ const Timekeeping = () => {
console.log(error) console.log(error)
} }
} }
// const handleUpdateCacheMonth = async () => {
// try {
// await update(
// updateCacheMonth,
// {
// year: date.year,
// month: date.month,
// },
// getTimeSheet,
// )
// } catch (error) {
// console.log(error)
// }
// }
useEffect(() => { useEffect(() => {
getTimeSheet() getTimeSheet()
@ -369,6 +405,19 @@ const Timekeeping = () => {
</div> </div>
) )
} }
const handleDelete = async (id: number) => {
try {
await Xdelete(
deleteNote,
{ id: id, month: date.month, year: date.year },
getTimeSheet,
)
} catch (error) {
console.log(error)
}
}
return ( return (
<div> <div>
<div className={classes.title}> <div className={classes.title}>
@ -510,6 +559,50 @@ const Timekeeping = () => {
> >
Save Save
</Button> </Button>
{customAddNotes.notes.map((item, index) => {
return (
<Box
key={index}
display="flex"
className="text-muted"
style={{
marginTop: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
marginBottom: '10px',
padding: '10px',
justifyContent: 'space-around',
}}
>
<Box w={'90%'}>
<TextInput
readOnly
variant="unstyled"
type="text"
size="xs"
label={`${item.reasonName} - ${item.timeTypeName}`}
w={'100%'}
value={item.note}
/>
</Box>
<Box
className={classes.optionIcon}
style={{ alignItems: 'center' }}
>
<IconTrash
className={classes.deleteIcon}
onClick={async () => {
await handleDelete(item.id)
// handleUpdateCacheMonth()
// close2()
}}
width={20}
height={20}
/>
</Box>
</Box>
)
})}
</Drawer> </Drawer>
<Box display={'flex'}> <Box display={'flex'}>
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}> <Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
@ -785,7 +878,11 @@ const Timekeeping = () => {
right={-3} right={-3}
display={notes && notes.length > 0 ? 'block' : 'none'} display={notes && notes.length > 0 ? 'block' : 'none'}
> >
<IconPointFilled width={15} height={15} style={{color:"#2767e1"}} /> <IconPointFilled
width={15}
height={15}
style={{ color: '#2767e1' }}
/>
</Box> </Box>
{total / 60 / 60 < 7 && {total / 60 / 60 < 7 &&
user.history.find( user.history.find(
@ -814,6 +911,13 @@ const Timekeeping = () => {
id: user.user.id, id: user.user.id,
name: user.user.name, name: user.user.name,
}, },
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
}) })
}} }}
/> />
@ -841,6 +945,13 @@ const Timekeeping = () => {
id: user.user.id, id: user.user.id,
name: user.user.name, name: user.user.name,
}, },
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
}) })
}} }}
/> />
@ -869,6 +980,13 @@ const Timekeeping = () => {
id: user.user.id, id: user.user.id,
name: user.user.name, name: user.user.name,
}, },
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
}) })
}} }}
/> />
@ -902,6 +1020,13 @@ const Timekeeping = () => {
id: user.user.id, id: user.user.id,
name: user.user.name, name: user.user.name,
}, },
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
}) })
}} }}
/> />
@ -925,6 +1050,13 @@ const Timekeeping = () => {
id: user.user.id, id: user.user.id,
name: user.user.name, name: user.user.name,
}, },
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
}) })
}} }}
/> />