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 Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Category;
use Modules\Admin\app\Models\Ticket;
use Modules\Admin\app\Models\Tracking;
class TicketController extends Controller
{
@ -288,7 +290,7 @@ class TicketController extends Controller
// Update updated_by and admin_note in tickets table
// Send notification email to users
$user = Admin::find($ticket->user_id);
if ($action == "confirm") {
foreach ($results as $result) {
list($year, $month, $day) = explode('-', $result['date']);
@ -301,6 +303,38 @@ class TicketController extends Controller
'n_reason' => $ticket->type,
'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;

View File

@ -161,4 +161,27 @@ class TimekeepingController extends Controller
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::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->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-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_year', $year)
->select(
'notes.id as n_id',
'n_user_id',
'n_day',
'n_month',

View File

@ -72,6 +72,7 @@ trait AnalyzeData
if (count($hasNotes) > 0) {
foreach ($hasNotes as $k_Note => $value_Note) {
$notes[$k_Note] = [
'id' => $value_Note->n_id,
'timeType' => $value_Note->n_time_type,
'timeTypeName' => $value_Note->time_type_name,
'reason' => $value_Note->n_reason,
@ -86,6 +87,7 @@ trait AnalyzeData
$notes = [];
foreach ($hasNotes as $k_Note => $value_Note) {
$notes[$k_Note] = [
'id' => $value_Note->n_id,
'timeType' => $value_Note->n_time_type,
'timeTypeName' => $value_Note->time_type_name,
'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 updateMultipleUserWorkingTime = API_URL + 'v1/admin/timekeeping/addMutilple'
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'
//Category

View File

@ -7,6 +7,7 @@ import {
Box,
Button,
Code,
Divider,
Group,
Modal,
PasswordInput,
@ -38,19 +39,36 @@ import classes from './NavbarSimpleColored.module.css'
const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
{ link: '/timekeeping', label: 'Timekeeping', icon: IconCalendar },
{ link: '/tracking', label: 'Check in/out', icon: IconScan },
{ link: '/worklogs', label: 'Worklogs', icon: IconReport },
{
link: '/timekeeping',
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',
label: 'Leave Management',
icon: IconCalendarClock,
group: 'normal',
},
{ link: '/tickets', label: 'Tickets', icon: IconTicket },
{ link: '/tickets', label: 'Tickets', icon: IconTicket, group: 'normal' },
{
link: '/tickets-management',
label: 'Tickets Management',
icon: IconDevices,
group: 'admin',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
@ -106,31 +124,90 @@ const Navbar = ({
getInitialValueInEffect: true,
})
const links = data.map((item) => {
if (user?.user?.permission !== 'admin' && item.link === '/tickets-management')
return null
// const links = data.map((item) => {
// if (
// 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 (
<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)
<div key={g.name}>
<Divider
display={
g.name === 'normal'
? 'block'
: user?.user?.permission === g.name
? 'block'
: 'none'
}
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span
className={`${classes.itemLabel} ${
isCompactMenu ? classes.labelCompactMenu : ''
} `}
>
{item.label}
</span>
</a>
my="xs"
label={
<span style={{ color: 'white', fontWeight: 700 }}>{g.label}</span>
}
labelPosition="center"
/>
{data
.filter((i) => {
return (
i.group === g.name &&
(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 { useEffect, useState } from 'react'
import classes from './TicketsManagement.module.css'
import { DateInput } from '@mantine/dates'
type TTickets = {
ticket_id: number
@ -52,17 +53,38 @@ interface DataReason {
c_name: string
}
interface DataTimeType {
id: number
c_code: string
c_name: string
}
interface FilterInfo {
key: string
name: string
type: string
}
const TicketsManagement = () => {
const [listTickets, setListTiskets] = useState<TListTickets>({
data: [],
})
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 [filter, setFilter] = useState({
typeReason: '',
statusFilter: '',
})
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([])
const getListMasterByType = async (type: string) => {
@ -86,6 +108,11 @@ const TicketsManagement = () => {
useEffect(() => {
const fetchData = async () => {
const resultTimeType = await getListMasterByType('TIME_TYPE')
setDataTimeType(
resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
)
const resultReason = await getListMasterByType('REASON')
setDataReason(resultReason)
}
@ -369,7 +396,7 @@ const TicketsManagement = () => {
label="Status"
data={[
{ value: 'WAITING', label: 'WAITING' },
{ value: 'CONFIRM', label: 'CONFIRM' },
{ value: 'CONFIRMED', label: 'CONFIRMED' },
{ value: 'REFUSED', label: 'REFUSED' },
]}
onChange={(e) => {
@ -411,6 +438,78 @@ const TicketsManagement = () => {
})}
>
<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
label="Admin Notes"
required

View File

@ -1,11 +1,12 @@
import {
deleteNote,
getListMaster,
getTheTimesheet,
updateMultipleUserWorkingTime,
updateNote,
updateWorkingDays,
updateWorkingDays
} from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import {
Box,
@ -19,7 +20,7 @@ import {
Text,
Textarea,
TextInput,
Tooltip,
Tooltip
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
@ -27,7 +28,8 @@ import {
IconCheck,
IconExclamationMark,
IconPointFilled,
IconX
IconTrash,
IconX,
} from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
@ -69,6 +71,7 @@ interface HistoryValue {
}
interface NoteValue {
id: number
note: string
reason: string
timeType: string
@ -113,6 +116,7 @@ const Timekeeping = () => {
reason: string
note: string
day: number
notes: NoteValue[]
}>({
user: {
id: 0,
@ -122,6 +126,7 @@ const Timekeeping = () => {
reason: '',
note: '',
day: 0,
notes: [],
})
const [workingDays, setWorkingDays] = useState(30)
@ -175,6 +180,23 @@ const Timekeeping = () => {
setData(
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(
Array.from({ length: getDaysInMonth() }, (_, index) => index + 1),
)
@ -283,6 +305,20 @@ const Timekeeping = () => {
console.log(error)
}
}
// const handleUpdateCacheMonth = async () => {
// try {
// await update(
// updateCacheMonth,
// {
// year: date.year,
// month: date.month,
// },
// getTimeSheet,
// )
// } catch (error) {
// console.log(error)
// }
// }
useEffect(() => {
getTimeSheet()
@ -369,6 +405,19 @@ const Timekeeping = () => {
</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 (
<div>
<div className={classes.title}>
@ -510,6 +559,50 @@ const Timekeeping = () => {
>
Save
</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>
<Box display={'flex'}>
<Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
@ -785,7 +878,11 @@ const Timekeeping = () => {
right={-3}
display={notes && notes.length > 0 ? 'block' : 'none'}
>
<IconPointFilled width={15} height={15} style={{color:"#2767e1"}} />
<IconPointFilled
width={15}
height={15}
style={{ color: '#2767e1' }}
/>
</Box>
{total / 60 / 60 < 7 &&
user.history.find(
@ -814,6 +911,13 @@ const Timekeeping = () => {
id: user.user.id,
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,
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,
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,
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,
name: user.user.name,
},
notes:
user.history.find(
(h) =>
h.day === d &&
h.notes &&
h.notes.length > 0,
)?.notes ?? [],
})
}}
/>