ManagementSystem/FRONTEND/src/pages/Timekeeping/Timekeeping.tsx

1027 lines
30 KiB
TypeScript

import {
deleteNote,
getListMaster,
getTheTimesheet,
updateMultipleUserWorkingTime,
updateNote,
updateWorkingDays,
} from '@/api/Admin'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import {
Avatar,
Box,
Button,
Drawer,
Image,
Menu,
MultiSelect,
Select,
Table,
Text,
Textarea,
TextInput,
Tooltip,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import {
IconCheck,
IconExclamationMark,
IconPointFilled,
IconTrash,
IconX,
} from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './Timekeeping.module.css'
interface User {
id: number
name: string
email: string
email_verified_at: string | null
permission: string
remember_token: string | null
avatar: string
created_at: string | null
updated_at: string | null
}
interface DataReason {
id: number
c_code: string
c_name: string
}
interface DataTimeType {
id: number
c_code: string
c_name: string
}
interface HistoryValue {
id: number
name: string
user_id: number
status: string
time_string: string
image: string
created_at: string
updated_at: string
}
interface NoteValue {
id: number
note: string
reason: string
timeType: string
reasonName: string
timeTypeName: string
}
interface History {
values: HistoryValue[]
total: number
day: number
notes: NoteValue[]
}
interface UserData {
user: User
history: History[]
}
const Timekeeping = () => {
const [opened1, { open: open1, close: close1 }] = useDisclosure(false)
const [opened2, { open: open2, close: close2 }] = useDisclosure(false)
const [disableBtn, setDisableBtn] = useState(false)
const [daysInMonth, setDaysInMonth] = useState(
Array.from({ length: 31 }, (_, index) => index + 1),
)
const [customAddData, setCustomAddData] = useState<{
data: string[]
type: string
day: number
}>({
data: [],
type: '',
day: 0,
})
const [customAddNotes, setCustomAddNotes] = useState<{
user: {
id: number
name: string
}
type: string
reason: string
note: string
day: number
notes: NoteValue[]
}>({
user: {
id: 0,
name: '',
},
type: '',
reason: '',
note: '',
day: 0,
notes: [],
})
const [workingDays, setWorkingDays] = useState(30)
const [data, setData] = useState<UserData[]>([])
const [date, setDate] = useState({
month: (new Date().getMonth() + 1).toString(),
year: new Date().getFullYear().toString(),
})
const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
const [dataReason, setDataReason] = useState<DataReason[]>([])
const getListMasterByType = async (type: string) => {
try {
const params = {
type: type,
}
const res = await get(getListMaster, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
useEffect(() => {
const fetchData = async () => {
const resultTimeType = await getListMasterByType('TIME_TYPE')
setDataTimeType(resultTimeType)
const resultReason = await getListMasterByType('REASON')
setDataReason(resultReason)
}
fetchData()
}, [])
const getTimeSheet = async () => {
try {
const res = await get(getTheTimesheet, {
month: date.month,
year: date.year,
})
if (res.status) {
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),
)
setWorkingDays(res.working_days ?? 30)
}
} catch (error: any) {
console.log(error)
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
}
function getDayName(dateString: string) {
// Tạo đối tượng Date từ chuỗi ngày tháng năm
const date = new Date(dateString)
// Lấy ngày trong tuần (0-6)
const dayNumber = date.getDay()
// Mảng chứa các tên ngày bằng tiếng Việt
const daysInVietnamese = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
// Trả về tên ngày bằng tiếng Việt
return daysInVietnamese[dayNumber]
}
function getDaysInMonth() {
// Tạo một đối tượng Date cho ngày đầu tiên của tháng tiếp theo
// Chú ý: tháng trong đối tượng Date là từ 0 (tháng 1) đến 11 (tháng 12)
const days = new Date(parseInt(date.year), parseInt(date.month), 0)
// Trả về ngày, tức là số ngày trong tháng
return days.getDate()
}
const updateMultipleUser = async (
users: number[],
day: number,
type: string,
) => {
try {
await update(
updateMultipleUserWorkingTime,
{
users: users,
year: date.year,
month: date.month,
day: day,
type: type,
},
getTimeSheet,
)
setDisableBtn(false)
} catch (error) {
console.log(error)
}
}
const updateInfoNote = async (
users: {
id: number
name: string
},
day: number,
type: string,
reason: string,
note: string,
) => {
try {
await update(
updateNote,
{
month: date.month,
year: date.year,
users: users,
type: type,
reason: reason,
note: note,
day: day,
},
getTimeSheet,
)
setDisableBtn(false)
} catch (error) {
console.log(error)
}
}
const handleUpdateWorkingDays = async () => {
try {
await update(
updateWorkingDays,
{
working_days: workingDays,
year: date.year,
month: date.month,
},
getTimeSheet,
)
} catch (error) {
console.log(error)
}
}
// const handleUpdateCacheMonth = async () => {
// try {
// await update(
// updateCacheMonth,
// {
// year: date.year,
// month: date.month,
// },
// getTimeSheet,
// )
// } catch (error) {
// console.log(error)
// }
// }
useEffect(() => {
getTimeSheet()
}, [date])
const showTooltipNote = (user: UserData, d: number) => {
return user.history
.find((h) => h.day === d && h.notes && h.notes.length > 0)
?.notes.map((v, index) => {
return (
<p key={index}>
- {v.reasonName} ({v.timeTypeName}): {v.note}
</p>
)
})
}
const showTooltipAllNote = (user: UserData) => {
return (
<table style={{ borderCollapse: 'collapse', width: '100%' }}>
<tbody>
{user.history
.filter((h) => h.notes && h.notes.length > 0)
.map((h, index) => (
<tr key={index} className={classes.historyRow}>
<td
style={{
border: '0',
verticalAlign: 'top',
fontWeight: 'bold',
}}
>
Day {h.day}:
</td>
<td style={{ border: '0', paddingLeft: '10px' }}>
{h.notes.map((v, noteIndex) => (
<p key={noteIndex} style={{ margin: '0' }}>
- {v.reasonName} ({v.timeTypeName}): {v.note}
</p>
))}
</td>
</tr>
))}
</tbody>
</table>
)
}
const showTooltip = (user: UserData, total: number, d: number) => {
return (
<div key={d}>
{`Total: ${(total / 60 / 60).toFixed(1)}h`}
{user.history
.find((h) => h.day === d)
?.values.map((v) => {
return (
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
key={v.id}
>
<p>{v.status + ': ' + v.time_string}</p>{' '}
{v.image && (
<Image
w={100}
h={100}
src={
import.meta.env.VITE_BACKEND_URL.includes('local')
? import.meta.env.VITE_BACKEND_URL +
'storage/' +
v.image
: import.meta.env.VITE_BACKEND_URL +
'image/storage/' +
v.image
}
/>
)}
</Box>
)
})}
{showTooltipNote(user, d)}
</div>
)
}
const handleDelete = async (id: number) => {
try {
await Xdelete(
deleteNote,
{ id: id, month: date.month, year: date.year },
getTimeSheet,
)
} catch (error) {
console.log(error)
}
}
const handleOpenNote = (user: UserData, d: number) => {
open2()
setCustomAddNotes({
...customAddNotes,
day: d,
user: {
id: user.user.id,
name: user.user.name,
},
notes:
user.history.find((h) => h.day === d && h.notes && h.notes.length > 0)
?.notes ?? [],
})
}
return (
<div>
<div className={classes.title}>
<h3>
Timekeeping
</h3>
</div>
<Drawer
opened={opened1}
onClose={close1}
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={() => {
setDisableBtn(true)
if (
customAddData.type === '' ||
customAddData.data.length === 0 ||
customAddData.day === 0
) {
notifications.show({
title: 'Error',
message: 'Input data required',
color: 'red',
})
setDisableBtn(false)
} else {
updateMultipleUser(
customAddData.data.map((u) => parseInt(u)),
customAddData.day,
customAddData.type,
)
}
}}
disabled={disableBtn}
>
Submit
</Button>
</Drawer>
{/* Form Save Note */}
<Drawer
opened={opened2}
onClose={close2}
position="right"
title={<strong>Save Note</strong>}
>
<p>
<span style={{ fontWeight: 'bold' }}>User</span>:{' '}
{customAddNotes.user.name}
<span style={{ paddingLeft: '10px', paddingRight: '10px' }}>|</span>
<span style={{ fontWeight: 'bold' }}>Day</span>: {customAddNotes.day}
</p>
<Select
mb={'md'}
searchable
label="Reason"
data={dataReason.map((user) => ({
value: user.c_code.toString(),
label: user.c_name,
}))}
onChange={(e) => {
setCustomAddNotes({ ...customAddNotes, reason: e! })
}}
/>
{customAddNotes.reason != '' && (
<Select
mb={'md'}
label="Time Type"
data={dataTimeType.map((item) => {
return { value: item.c_code.toString(), label: item.c_name }
})}
onChange={(e) => {
setCustomAddNotes({ ...customAddNotes, type: e! })
}}
/>
)}
{customAddNotes.type != '' && (
<Textarea
mb={'md'}
label="Note"
onChange={(e) => {
setCustomAddNotes({ ...customAddNotes, note: e.target.value })
}}
/>
)}
<Button
onClick={() => {
setDisableBtn(true)
if (
customAddNotes.type === '' ||
customAddNotes.reason === '' ||
customAddNotes.note === '' ||
customAddNotes.day === 0 ||
customAddNotes.user.id === 0
) {
notifications.show({
title: 'Error',
message: 'Input data required',
color: 'red',
})
setDisableBtn(false)
} else {
updateInfoNote(
customAddNotes.user,
customAddNotes.day,
customAddNotes.type,
customAddNotes.reason,
customAddNotes.note,
)
}
}}
disabled={disableBtn}
>
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%'}>
<Box w="100%" display={'flex'}>
<Select
w="50%"
value={date.month}
size="xs"
label="Month"
data={Array.from({ length: 12 }, (_, index) => {
return {
value: (1 + index).toString(),
label: (1 + index).toString(),
disabled:
1 + index > parseInt(moment(Date.now()).format('MM')),
}
})}
onChange={(e) => {
setDate({ ...date, month: e! })
}}
></Select>
<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 display={'flex'} style={{ alignItems: 'end' }}>
<TextInput
type="number"
size="xs"
label="Working days"
w={'20%'}
value={workingDays}
onChange={(e) => {
setWorkingDays(parseFloat(e.target.value))
}}
/>
<Tooltip label="Save working days">
<Button
size="xs"
ml={'sm'}
onClick={() => handleUpdateWorkingDays()}
>
Save
</Button>
</Tooltip>
</Box>
</Box>
<Box
w="70%"
pl={200}
style={{
display: 'flex',
alignItems: 'end',
justifyContent: 'space-evenly',
}}
>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<IconCheck
size={20}
style={{
backgroundColor: 'green',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
/>
<Text ml={'sm'} fz="14px">
Work over 7 hours
</Text>
</Box>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<IconCheck
size={20}
style={{
backgroundColor: 'orange',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
/>
<Text ml={'sm'} fz="14px">
Work over 3.5 hours
</Text>
</Box>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<IconExclamationMark
size={20}
style={{
backgroundColor: 'orange',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
/>
<Text ml={'sm'} fz="14px">
Checked in
</Text>
</Box>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<IconX
size={20}
style={{
backgroundColor: '#ff4646',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
/>
<Text ml={'sm'} fz="14px">
Off
</Text>
</Box>
</Box>
</Box>
<Box>
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
mt={'md'}
>
<Table.Thead>
<Table.Tr bg={'#228be66b'}>
<Table.Th>Day</Table.Th>
<Table.Th></Table.Th>
<Table.Th></Table.Th>
{daysInMonth.map((d) => {
return (
<Menu width={200} shadow="md" key={d}>
<Menu.Target>
<Table.Th
key={d}
ta={'center'}
style={{ cursor: 'pointer' }}
>
<span>{d}</span>
</Table.Th>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={() =>
updateMultipleUser(
data.map((u) => u.user.id),
d,
'half',
)
}
>
+ 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={() => {
open1()
setCustomAddData({ ...customAddData, day: d })
}}
>
+ Add custom worklog
</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
)
})}
</Table.Tr>
<Table.Tr bg={'#228be66b'}>
<Table.Th></Table.Th>
<Table.Th ta={'center'}>Total</Table.Th>
<Table.Th ta={'center'}>Off</Table.Th>
{daysInMonth.map((d) => {
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%)'
: ''
}
>
{getDayName(`${date.year}-${date.month}-${d}`)}
</Table.Th>
)
})}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{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
return (
<Table.Tr key={user.user.id} className={classes.tableTr}>
<Table.Td>
<Tooltip
// position={'auto'}
multiline
// opened
// offset={{ mainAxis: 5, crossAxis: 0 }}
label={showTooltipAllNote(user)}
>
<div style={{display:'flex', alignItems:'center'}}><Avatar size={'md'} 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/' +
user.user.avatar}/>{user.user.name}</div>
</Tooltip>
</Table.Td>
<Table.Td ta={'center'}>{totalDays}</Table.Td>
<Table.Td ta={'center'}>{workingDays - totalDays}</Table.Td>
{daysInMonth.map((d) => {
var total =
user.history.find((h) => h.day === d)?.total ?? 0
var notes = user.history.find((h) => h.day === d)?.notes
return (
<Table.Td
pos={'relative'}
key={d}
ta={'center'}
bg={
getDayName(`${date.year}-${date.month}-${d}`) ===
'Su' ||
getDayName(`${date.year}-${date.month}-${d}`) === 'Sa'
? 'rgb(251 255 196 / 78%)'
: ''
}
>
<Box
pos={'absolute'}
top={-3}
right={-3}
display={notes && notes.length > 0 ? 'block' : 'none'}
>
<IconPointFilled
width={15}
height={15}
style={{ color: '#2767e1' }}
/>
</Box>
{total / 60 / 60 < 7 &&
user.history.find(
(h) => h.day === d && h.values && h.values.length > 0,
) ? (
total / 60 / 60 >= 3.5 ? (
<Tooltip
multiline
label={showTooltip(user, total, d)}
>
<IconCheck
size={20}
style={{
backgroundColor: 'orange',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
onClick={() => {
handleOpenNote(user, d)
}}
/>
</Tooltip>
) : (
<Tooltip
multiline
label={showTooltip(user, total, d)}
>
<IconExclamationMark
size={20}
style={{
backgroundColor: 'orange',
color: 'white',
borderRadius: '5px',
padding: '2px',
fontWeight: 700,
}}
onClick={() => {
handleOpenNote(user, d)
}}
/>
</Tooltip>
)
) : total >= 7 ? (
<>
<Tooltip
multiline
label={showTooltip(user, total, d)}
>
<IconCheck
size={20}
style={{
backgroundColor: 'green',
color: 'white',
borderRadius: '5px',
padding: '2px',
}}
onClick={() => {
handleOpenNote(user, d)
}}
/>
</Tooltip>
</>
) : (
<>
{user.history.find(
(h) =>
h.day === d && h.notes && h.notes.length > 0,
) ? (
<Tooltip
multiline
label={showTooltipNote(user, d)}
>
<IconX
size={20}
style={{
backgroundColor: '#ff4646',
color: 'white',
borderRadius: '5px',
padding: '2px',
}}
id={`indexBySN${user.user.id}_${d}`}
onClick={() => {
handleOpenNote(user, d)
}}
/>
</Tooltip>
) : (
<IconX
size={20}
style={{
backgroundColor: '#ff4646',
color: 'white',
borderRadius: '5px',
padding: '2px',
}}
id={`indexBySN${user.user.id}_${d}`}
onClick={() => {
handleOpenNote(user, d)
}}
/>
)}
</>
)}
</Table.Td>
)
})}
</Table.Tr>
)
})}
</Table.Tbody>
</Table>
</Box>
</div>
)
}
export default Timekeeping