474 lines
18 KiB
TypeScript
474 lines
18 KiB
TypeScript
import { getTheTimesheet, updateMultipleUserWorkingTime } from '@/api/Admin'
|
|
import { get } from '@/rtk/helpers/apiService'
|
|
import { Box, Image, Menu, Select, Table, Text, TextInput, Tooltip } from '@mantine/core'
|
|
import { IconCheck, IconExclamationMark, IconX } from '@tabler/icons-react'
|
|
import { useEffect, useState } from 'react'
|
|
import classes from './Timekeeping.module.css'
|
|
import { notifications } from '@mantine/notifications'
|
|
import moment from 'moment'
|
|
import { update } from '@/rtk/helpers/CRUD'
|
|
|
|
interface User {
|
|
id: number
|
|
name: string
|
|
email: string
|
|
email_verified_at: string | null
|
|
permission: string
|
|
remember_token: string | null
|
|
created_at: string | null
|
|
updated_at: string | null
|
|
}
|
|
|
|
interface HistoryValue {
|
|
id: number
|
|
name: string
|
|
user_id: number
|
|
status: string
|
|
time_string: string
|
|
image: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
interface History {
|
|
values: HistoryValue[]
|
|
total: number
|
|
day: number
|
|
}
|
|
|
|
interface UserData {
|
|
user: User
|
|
history: History[]
|
|
}
|
|
|
|
const Timekeeping = () => {
|
|
const [daysInMonth, setDaysInMonth] = useState(
|
|
Array.from({ length: 31 }, (_, index) => index + 1),
|
|
)
|
|
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 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'),
|
|
),
|
|
)
|
|
setDaysInMonth(
|
|
Array.from({ length: getDaysInMonth() }, (_, index) => index + 1),
|
|
)
|
|
|
|
localStorage.getItem('workingdays') ? setWorkingDays(parseFloat(localStorage.getItem('workingdays')!)) : setWorkingDays(getDaysInMonth())
|
|
}
|
|
} 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)
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
getTimeSheet()
|
|
}, [date])
|
|
|
|
return (
|
|
<div>
|
|
<div className={classes.title}>
|
|
<h3>
|
|
<Text>Admin/</Text>
|
|
Timekeeping
|
|
</h3>
|
|
</div>
|
|
<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) =>
|
|
(1 + index).toString(),
|
|
)}
|
|
onChange={(e) => {
|
|
setDate({ ...date, month: e! })
|
|
}}
|
|
></Select>
|
|
<Select
|
|
w="50%"
|
|
value={date.year}
|
|
size="xs"
|
|
ml={'sm'}
|
|
label="Year"
|
|
data={Array.from({ length: 20 }, (_, index) =>
|
|
(2023 + 1 + index).toString(),
|
|
)}
|
|
onChange={(e) => {
|
|
setDate({ ...date, year: e! })
|
|
}}
|
|
></Select>
|
|
</Box>
|
|
<Box>
|
|
<TextInput type='number' size='xs' label="Working days" w={"20%"} value={workingDays} onChange={(e)=>{
|
|
setWorkingDays(parseFloat(e.target.value))
|
|
localStorage.setItem('workingdays', e.target.value)
|
|
}}/>
|
|
</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">
|
|
<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.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>{user.user.name}</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
|
|
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%)' : ''}>
|
|
{total / 60 / 60 < 7 && user.history.find((h) => h.day === d) ?
|
|
total / 60 / 60 >= 3.5 ?
|
|
<Tooltip
|
|
multiline
|
|
label={
|
|
<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>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
>
|
|
<IconCheck
|
|
size={20}
|
|
style={{
|
|
backgroundColor: 'orange',
|
|
color: 'white',
|
|
borderRadius: '5px',
|
|
padding: '2px',
|
|
fontWeight: 700,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
: <Tooltip
|
|
multiline
|
|
label={
|
|
<div>
|
|
{`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>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
>
|
|
<IconExclamationMark
|
|
size={20}
|
|
style={{
|
|
backgroundColor: 'orange',
|
|
color: 'white',
|
|
borderRadius: '5px',
|
|
padding: '2px',
|
|
fontWeight: 700,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
: total >= 7 ? (
|
|
<Tooltip
|
|
multiline
|
|
label={
|
|
<div>
|
|
{`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>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
>
|
|
<IconCheck
|
|
size={20}
|
|
style={{
|
|
backgroundColor: 'green',
|
|
color: 'white',
|
|
borderRadius: '5px',
|
|
padding: '2px',
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
) : (
|
|
parseInt(moment(Date.now()).format('DD')) >= d &&
|
|
<IconX
|
|
size={20}
|
|
style={{
|
|
backgroundColor: '#ff4646',
|
|
color: 'white',
|
|
borderRadius: '5px',
|
|
padding: '2px',
|
|
}}
|
|
/>
|
|
)}
|
|
</Table.Td>
|
|
)
|
|
})}
|
|
</Table.Tr>
|
|
)
|
|
})}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Box>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Timekeeping
|