update custom add worklog

This commit is contained in:
JOSEPH LE 2024-07-27 08:24:46 +07:00
parent e7e81dead7
commit 0ea97cb0c5
2 changed files with 305 additions and 142 deletions

View File

@ -86,7 +86,7 @@ class TimekeepingController extends Controller
] ]
]); ]);
} }
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['status' => true, 'message' => 'Add successfully']); return response()->json(['status' => true, 'message' => 'Add successfully']);
} }

View File

@ -1,12 +1,29 @@
import { getTheTimesheet, updateMultipleUserWorkingTime, updateWorkingDays } from '@/api/Admin' import {
getTheTimesheet,
updateMultipleUserWorkingTime,
updateWorkingDays,
} from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD' import { update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService' import { get } from '@/rtk/helpers/apiService'
import { Box, Button, Image, Menu, Select, Table, Text, TextInput, Tooltip } from '@mantine/core' import {
Box,
Button,
Drawer,
Image,
Menu,
MultiSelect,
Select,
Table,
Text,
TextInput,
Tooltip
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications' import { notifications } from '@mantine/notifications'
import { IconCheck, IconExclamationMark, IconX } from '@tabler/icons-react' import { IconCheck, IconExclamationMark, IconX } from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import classes from './Timekeeping.module.css' import classes from './Timekeeping.module.css'
import moment from 'moment'
interface User { interface User {
id: number id: number
@ -42,9 +59,15 @@ interface UserData {
} }
const Timekeeping = () => { const Timekeeping = () => {
const [opened, { open, close }] = useDisclosure(false)
const [daysInMonth, setDaysInMonth] = useState( const [daysInMonth, setDaysInMonth] = useState(
Array.from({ length: 31 }, (_, index) => index + 1), Array.from({ length: 31 }, (_, index) => index + 1),
) )
const [customAddData, setCustomAddData] = useState<{data:string[], type: string, day: number}>({
data: [],
type: '',
day: 0
})
const [workingDays, setWorkingDays] = useState(30) const [workingDays, setWorkingDays] = useState(30)
const [data, setData] = useState<UserData[]>([]) const [data, setData] = useState<UserData[]>([])
const [date, setDate] = useState({ const [date, setDate] = useState({
@ -60,9 +83,7 @@ const Timekeeping = () => {
}) })
if (res.status) { if (res.status) {
setData( setData(
res.data.filter((u: UserData) => res.data.filter((u: UserData) => u.user.permission.includes('staff')),
u.user.permission.includes('staff'),
),
) )
setDaysInMonth( setDaysInMonth(
Array.from({ length: getDaysInMonth() }, (_, index) => index + 1), Array.from({ length: getDaysInMonth() }, (_, index) => index + 1),
@ -102,27 +123,39 @@ const Timekeeping = () => {
return days.getDate() return days.getDate()
} }
const updateMultipleUser = async(users:number[], day:number, type:string)=>{ const updateMultipleUser = async (
users: number[],
day: number,
type: string,
) => {
try { try {
await update(updateMultipleUserWorkingTime, { await update(
users: users, updateMultipleUserWorkingTime,
year: date.year, {
month: date.month, users: users,
day:day, year: date.year,
type: type month: date.month,
}, getTimeSheet) day: day,
type: type,
},
getTimeSheet,
)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
} }
const handleUpdateWorkingDays = async()=>{ const handleUpdateWorkingDays = async () => {
try { try {
await update(updateWorkingDays, { await update(
working_days: workingDays, updateWorkingDays,
year: date.year, {
month: date.month working_days: workingDays,
}, getTimeSheet) year: date.year,
month: date.month,
},
getTimeSheet,
)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -132,6 +165,7 @@ const Timekeeping = () => {
getTimeSheet() getTimeSheet()
}, [date]) }, [date])
console.log(customAddData)
return ( return (
<div> <div>
<div className={classes.title}> <div className={classes.title}>
@ -140,24 +174,58 @@ const Timekeeping = () => {
Timekeeping Timekeeping
</h3> </h3>
</div> </div>
<Drawer
opened={opened}
onClose={close}
position="right"
title={<strong>Add custom worklog</strong>}
>
<MultiSelect
mb={'md'}
searchable
label="User(s)"
data={data.map((user) => {
return { value: user.user.id.toString(), label: user.user.name }
})}
onChange={(e)=>{setCustomAddData({...customAddData, data: e})}}
/>
<Select
mb={'md'}
label="Type"
data={[
{ value: 'half', label: 'Half day' },
{ value: 'one', label: 'A day' },
]}
onChange={(e)=>{setCustomAddData({...customAddData, type: e!})}}
/>
<Button onClick={()=>{
if(customAddData.type === "" || customAddData.data.length === 0 || customAddData.day === 0){
notifications.show({
title: 'Error',
message: "Input data required",
color: 'red',
})
}else{
updateMultipleUser(customAddData.data.map((u)=>parseInt(u)), customAddData.day, customAddData.type)
}
}}>Submit</Button>
</Drawer>
<Box display={'flex'}> <Box display={'flex'}>
<Box style={{display:"flex", flexFlow:"column"}} w={'30%'}> <Box style={{ display: 'flex', flexFlow: 'column' }} w={'30%'}>
<Box w="100%" display={'flex'}> <Box w="100%" display={'flex'}>
<Select <Select
w="50%" w="50%"
value={date.month} value={date.month}
size="xs" size="xs"
label="Month" label="Month"
data={Array.from({ length: 12 }, (_, index) => data={Array.from({ length: 12 }, (_, index) => {
{ return {
return { value: (1 + index).toString(),
value: (1 + index).toString(), label: (1 + index).toString(),
label:(1 + index).toString(), disabled:
disabled: (1 + index) > parseInt(moment(Date.now()).format('MM')) 1 + index > parseInt(moment(Date.now()).format('MM')),
}
} }
})}
)}
onChange={(e) => { onChange={(e) => {
setDate({ ...date, month: e! }) setDate({ ...date, month: e! })
}} }}
@ -168,29 +236,60 @@ const Timekeeping = () => {
size="xs" size="xs"
ml={'sm'} ml={'sm'}
label="Year" label="Year"
data={Array.from({ length: 10 }, (_, index) => data={Array.from({ length: 10 }, (_, index) => {
{ return {
return { value: (
value: (parseInt(moment(Date.now()).format('YYYY')) - 3 + index).toString(), parseInt(moment(Date.now()).format('YYYY')) -
label:(parseInt(moment(Date.now()).format('YYYY')) - 3 + index).toString(), 3 +
disabled: (parseInt(moment(Date.now()).format('YYYY')) - 3 + index) > parseInt(moment(Date.now()).format('YYYY')) 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) => { onChange={(e) => {
setDate({ ...date, year: e! }) setDate({ ...date, year: e! })
}} }}
></Select> ></Select>
</Box> </Box>
<Box display={'flex'} style={{alignItems:'end'}}> <Box display={'flex'} style={{ alignItems: 'end' }}>
<TextInput type='number' size='xs' label="Working days" w={"20%"} value={workingDays} onChange={(e)=>{ <TextInput
type="number"
size="xs"
label="Working days"
w={'20%'}
value={workingDays}
onChange={(e) => {
setWorkingDays(parseFloat(e.target.value)) setWorkingDays(parseFloat(e.target.value))
}}/> }}
<Tooltip label="Save working days"><Button size='xs' ml={'sm'} onClick={()=>handleUpdateWorkingDays()}>Save</Button></Tooltip> />
</Box> <Tooltip label="Save working days">
<Button
size="xs"
ml={'sm'}
onClick={() => handleUpdateWorkingDays()}
>
Save
</Button>
</Tooltip>
</Box>
</Box> </Box>
<Box w="70%" pl={200} style={{display:'flex', alignItems:'end', justifyContent:"space-evenly"}}> <Box
<Box style={{display:'flex', alignItems:'center'}}> w="70%"
pl={200}
style={{
display: 'flex',
alignItems: 'end',
justifyContent: 'space-evenly',
}}
>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<IconCheck <IconCheck
size={20} size={20}
style={{ style={{
@ -201,9 +300,11 @@ const Timekeeping = () => {
fontWeight: 700, fontWeight: 700,
}} }}
/> />
<Text ml={'sm'} fz='14px'>Work over 7 hours</Text> <Text ml={'sm'} fz="14px">
Work over 7 hours
</Text>
</Box> </Box>
<Box style={{display:'flex', alignItems:'center'}}> <Box style={{ display: 'flex', alignItems: 'center' }}>
<IconCheck <IconCheck
size={20} size={20}
style={{ style={{
@ -214,9 +315,11 @@ const Timekeeping = () => {
fontWeight: 700, fontWeight: 700,
}} }}
/> />
<Text ml={'sm'} fz='14px'>Work over 3.5 hours</Text> <Text ml={'sm'} fz="14px">
Work over 3.5 hours
</Text>
</Box> </Box>
<Box style={{display:'flex', alignItems:'center'}}> <Box style={{ display: 'flex', alignItems: 'center' }}>
<IconExclamationMark <IconExclamationMark
size={20} size={20}
style={{ style={{
@ -227,9 +330,11 @@ const Timekeeping = () => {
fontWeight: 700, fontWeight: 700,
}} }}
/> />
<Text ml={'sm'} fz='14px'>Checked in</Text> <Text ml={'sm'} fz="14px">
Checked in
</Text>
</Box> </Box>
<Box style={{display:'flex', alignItems:'center'}}> <Box style={{ display: 'flex', alignItems: 'center' }}>
<IconX <IconX
size={20} size={20}
style={{ style={{
@ -240,7 +345,9 @@ const Timekeeping = () => {
fontWeight: 700, fontWeight: 700,
}} }}
/> />
<Text ml={'sm'} fz='14px'>Off</Text> <Text ml={'sm'} fz="14px">
Off
</Text>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -260,23 +367,49 @@ const Timekeeping = () => {
{daysInMonth.map((d) => { {daysInMonth.map((d) => {
return ( return (
<Menu width={200} shadow="md"> <Menu width={200} shadow="md">
<Menu.Target> <Menu.Target>
<Table.Th key={d} ta={'center'} style={{cursor:"pointer"}}> <Table.Th
key={d}
ta={'center'}
style={{ cursor: 'pointer' }}
>
<span>{d}</span> <span>{d}</span>
</Table.Th> </Table.Th>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item onClick={()=>updateMultipleUser(data.map((u)=>u.user.id), d, 'half')}> <Menu.Item
+ Add half a day's work onClick={() =>
</Menu.Item> updateMultipleUser(
<Menu.Item data.map((u) => u.user.id),
onClick={()=>updateMultipleUser(data.map((u)=>u.user.id), d, 'one')} d,
> 'half',
+ Add 1 day of work )
</Menu.Item> }
</Menu.Dropdown> >
</Menu> + Add half a day's work
</Menu.Item>
<Menu.Item
onClick={() =>
updateMultipleUser(
data.map((u) => u.user.id),
d,
'one',
)
}
>
+ Add 1 day of work
</Menu.Item>
<Menu.Item>
<Text size="sm" onClick={()=>{
open()
setCustomAddData({...customAddData, day: d})
}}>
+ Add custom worklog
</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
) )
})} })}
</Table.Tr> </Table.Tr>
@ -286,7 +419,16 @@ const Timekeeping = () => {
<Table.Th ta={'center'}>Off</Table.Th> <Table.Th ta={'center'}>Off</Table.Th>
{daysInMonth.map((d) => { {daysInMonth.map((d) => {
return ( return (
<Table.Th key={d} ta={'center'} bg={(getDayName(`${date.year}-${date.month}-${d}`)=== "Su" || getDayName(`${date.year}-${date.month}-${d}`) === "Sa") ? 'rgb(251 255 196 / 78%)' : ''}> <Table.Th
key={d}
ta={'center'}
bg={
getDayName(`${date.year}-${date.month}-${d}`) === 'Su' ||
getDayName(`${date.year}-${date.month}-${d}`) === 'Sa'
? 'rgb(251 255 196 / 78%)'
: ''
}
>
{getDayName(`${date.year}-${date.month}-${d}`)} {getDayName(`${date.year}-${date.month}-${d}`)}
</Table.Th> </Table.Th>
) )
@ -295,19 +437,35 @@ const Timekeeping = () => {
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{data.map((user) => { {data.map((user) => {
let totalDays = user.history.filter((h) => h.total / 60 / 60 >= 7).length + (user.history.filter((h) => h.total / 60 / 60 < 7 && h.total / 60 / 60 >= 3.5).length / 2) let totalDays =
user.history.filter((h) => h.total / 60 / 60 >= 7).length +
user.history.filter(
(h) => h.total / 60 / 60 < 7 && h.total / 60 / 60 >= 3.5,
).length /
2
return ( return (
<Table.Tr key={user.user.id} className={classes.tableTr}> <Table.Tr key={user.user.id} className={classes.tableTr}>
<Table.Td>{user.user.name}</Table.Td> <Table.Td>{user.user.name}</Table.Td>
<Table.Td ta={'center'}>{totalDays}</Table.Td> <Table.Td ta={'center'}>{totalDays}</Table.Td>
<Table.Td ta={'center'}>{workingDays-totalDays}</Table.Td> <Table.Td ta={'center'}>{workingDays - totalDays}</Table.Td>
{daysInMonth.map((d) => { {daysInMonth.map((d) => {
var total = var total =
user.history.find((h) => h.day === d)?.total ?? 0 user.history.find((h) => h.day === d)?.total ?? 0
return ( return (
<Table.Td key={d} ta={'center'} bg={(getDayName(`${date.year}-${date.month}-${d}`)=== "Su" || getDayName(`${date.year}-${date.month}-${d}`) === "Sa") ? 'rgb(251 255 196 / 78%)' : ''}> <Table.Td
{total / 60 / 60 < 7 && user.history.find((h) => h.day === d) ? key={d}
total / 60 / 60 >= 3.5 ? ta={'center'}
bg={
getDayName(`${date.year}-${date.month}-${d}`) ===
'Su' ||
getDayName(`${date.year}-${date.month}-${d}`) === 'Sa'
? 'rgb(251 255 196 / 78%)'
: ''
}
>
{total / 60 / 60 < 7 &&
user.history.find((h) => h.day === d) ? (
total / 60 / 60 >= 3.5 ? (
<Tooltip <Tooltip
multiline multiline
label={ label={
@ -325,7 +483,9 @@ const Timekeeping = () => {
}} }}
key={v.id} key={v.id}
> >
<p>{v.status + ': ' + v.time_string}</p>{' '} <p>
{v.status + ': ' + v.time_string}
</p>{' '}
{v.image && ( {v.image && (
<Image <Image
w={100} w={100}
@ -335,13 +495,13 @@ const Timekeeping = () => {
'local', 'local',
) )
? import.meta.env ? import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'storage/' + 'storage/' +
v.image v.image
: import.meta.env : import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'image/storage/' + 'image/storage/' +
v.image v.image
} }
/> />
)} )}
@ -362,7 +522,8 @@ const Timekeeping = () => {
}} }}
/> />
</Tooltip> </Tooltip>
: <Tooltip ) : (
<Tooltip
multiline multiline
label={ label={
<div> <div>
@ -379,7 +540,9 @@ const Timekeeping = () => {
}} }}
key={v.id} key={v.id}
> >
<p>{v.status + ': ' + v.time_string}</p>{' '} <p>
{v.status + ': ' + v.time_string}
</p>{' '}
{v.image && ( {v.image && (
<Image <Image
w={100} w={100}
@ -389,13 +552,13 @@ const Timekeeping = () => {
'local', 'local',
) )
? import.meta.env ? import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'storage/' + 'storage/' +
v.image v.image
: import.meta.env : import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'image/storage/' + 'image/storage/' +
v.image v.image
} }
/> />
)} )}
@ -416,72 +579,72 @@ const Timekeeping = () => {
}} }}
/> />
</Tooltip> </Tooltip>
: total >= 7 ? ( )
<Tooltip ) : total >= 7 ? (
multiline <Tooltip
label={ multiline
<div> label={
{`Total: ${(total / 60 / 60).toFixed(1)}h`} <div>
{user.history {`Total: ${(total / 60 / 60).toFixed(1)}h`}
.find((h) => h.day === d) {user.history
?.values.map((v) => { .find((h) => h.day === d)
return ( ?.values.map((v) => {
<Box return (
style={{ <Box
display: 'flex', style={{
alignItems: 'center', display: 'flex',
justifyContent: 'space-between', alignItems: 'center',
}} justifyContent: 'space-between',
key={v.id} }}
> key={v.id}
<p>{v.status + ': ' + v.time_string}</p>{' '} >
{v.image && ( <p>{v.status + ': ' + v.time_string}</p>{' '}
<Image {v.image && (
w={100} <Image
h={100} w={100}
src={ h={100}
import.meta.env.VITE_BACKEND_URL.includes( src={
'local', import.meta.env.VITE_BACKEND_URL.includes(
) 'local',
? import.meta.env )
? import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'storage/' + 'storage/' +
v.image v.image
: import.meta.env : import.meta.env
.VITE_BACKEND_URL + .VITE_BACKEND_URL +
'image/storage/' + 'image/storage/' +
v.image v.image
} }
/> />
)} )}
</Box> </Box>
) )
})} })}
</div> </div>
} }
> >
<IconCheck <IconCheck
size={20}
style={{
backgroundColor: 'green',
color: 'white',
borderRadius: '5px',
padding: '2px',
}}
/>
</Tooltip>
) : (
<IconX
size={20} size={20}
style={{ style={{
backgroundColor: '#ff4646', backgroundColor: 'green',
color: 'white', color: 'white',
borderRadius: '5px', borderRadius: '5px',
padding: '2px', padding: '2px',
}} }}
/> />
)} </Tooltip>
) : (
<IconX
size={20}
style={{
backgroundColor: '#ff4646',
color: 'white',
borderRadius: '5px',
padding: '2px',
}}
/>
)}
</Table.Td> </Table.Td>
) )
})} })}