477 lines
18 KiB
TypeScript
477 lines
18 KiB
TypeScript
import { getAllUserDoing } from '@/api/Admin'
|
|
import { get } from '@/rtk/helpers/apiService'
|
|
import {
|
|
Avatar,
|
|
Badge,
|
|
Box,
|
|
Card,
|
|
Loader,
|
|
Modal,
|
|
Popover,
|
|
Text,
|
|
Tooltip,
|
|
} from '@mantine/core'
|
|
import { IconInnerShadowTopRightFilled } from '@tabler/icons-react'
|
|
import { useEffect, useState } from 'react'
|
|
import classes from './Allocation.module.css'
|
|
interface UserInfo {
|
|
self: string
|
|
accountId: string
|
|
avatarUrls: AvatarUrls
|
|
displayName: string
|
|
active: boolean
|
|
timeZone: string
|
|
accountType: string
|
|
}
|
|
|
|
interface AvatarUrls {
|
|
'48x48': string
|
|
'24x24': string
|
|
'16x16': string
|
|
'32x32': string
|
|
}
|
|
|
|
interface Issue {
|
|
expand: string
|
|
id: string
|
|
self: string
|
|
key: string
|
|
changelog: any
|
|
fields: IssueFields
|
|
}
|
|
|
|
interface IssueFields {
|
|
summary: string
|
|
timespent: number
|
|
timeoriginalestimate: number
|
|
project: ProjectInfo
|
|
status: StatusInfo
|
|
updated: string
|
|
}
|
|
|
|
interface ProjectInfo {
|
|
self: string
|
|
id: string
|
|
key: string
|
|
name: string
|
|
projectTypeKey: string
|
|
simplified: boolean
|
|
avatarUrls: AvatarUrls
|
|
}
|
|
interface StatusCategory {
|
|
self: string
|
|
id: number
|
|
key: string
|
|
colorName: string
|
|
name: string
|
|
}
|
|
|
|
interface StatusInfo {
|
|
self: string
|
|
description: string
|
|
iconUrl: string
|
|
name: string
|
|
id: string
|
|
statusCategory: StatusCategory
|
|
}
|
|
|
|
const Allocation = () => {
|
|
const [loading, setLoading] = useState(true)
|
|
const [opened, setOpened] = useState(false)
|
|
const [issDetail, setIssDetail] = useState('')
|
|
const [data, setData] = useState<any>({})
|
|
const getAll = async () => {
|
|
try {
|
|
const res = await get(getAllUserDoing)
|
|
if (res.status) {
|
|
setLoading(false)
|
|
setData(res.data)
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
// console.log(data)
|
|
|
|
const getStateChanges = (changelog: any) => {
|
|
return changelog.histories
|
|
.map((history: any) => {
|
|
const author = history.author.displayName
|
|
const created = history.created
|
|
const changes = history.items
|
|
.map((item: any) => {
|
|
return `${item.field} changed from ${item.fromString} ➔ ${item.toString}`
|
|
})
|
|
.join(', ')
|
|
|
|
return changes
|
|
? `${author} made changes on ${created}: ${changes}`
|
|
: null
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
}
|
|
|
|
useEffect(() => {
|
|
getAll()
|
|
}, [])
|
|
return (
|
|
<div>
|
|
<div>
|
|
<Box
|
|
style={{
|
|
marginTop: '20%',
|
|
textAlign: 'center',
|
|
display: loading ? 'block' : 'none',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="grape" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Analyzing . . .
|
|
</Text>
|
|
</Box>
|
|
<Box
|
|
style={{
|
|
display: !loading ? 'flex' : 'none',
|
|
flexFlow: 'column',
|
|
}}
|
|
>
|
|
<Box>
|
|
<Text fw={600} fz={'md'}>
|
|
Admin/
|
|
</Text>
|
|
<Text fw={700} fz={'lg'}>
|
|
Personnel Allocation
|
|
</Text>
|
|
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
|
|
"P:" is the timspent/timeestimate number within the project itself
|
|
</Text>
|
|
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
|
|
"A:" is the timspent/timeestimate of all projects
|
|
</Text>
|
|
</Box>
|
|
<Box
|
|
style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-evenly',
|
|
}}
|
|
>
|
|
{!loading &&
|
|
Object.keys(data?.projects).map((pro) => {
|
|
return (
|
|
<Box w={'32%'} mt={'xl'}>
|
|
<Card
|
|
shadow="md"
|
|
padding="xl"
|
|
component="a"
|
|
target="_blank"
|
|
style={{
|
|
minHeight: '28vh',
|
|
border: 'solid 1px rgb(28 74 115 / 64%)',
|
|
}}
|
|
>
|
|
<Card.Section
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '5px 20px',
|
|
backgroundColor: '#1c4a73',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Avatar
|
|
size={'sm'}
|
|
src={data.projects[pro].project.avatarUrls['48x48']}
|
|
/>
|
|
<Text ml={'md'} fw={700} size="lg">
|
|
{pro}
|
|
</Text>
|
|
</Card.Section>
|
|
|
|
{!loading &&
|
|
Object.keys(data?.projects[pro].users).map((user) => {
|
|
const userData = data.projects[pro].users[user]
|
|
return (
|
|
<Box style={{ cursor: 'pointer' }}>
|
|
<Popover
|
|
width={'25%'}
|
|
position="bottom"
|
|
withArrow
|
|
shadow="md"
|
|
>
|
|
<Popover.Target>
|
|
<Box
|
|
mt={'sm'}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
display: 'flex',
|
|
width: '60%',
|
|
backgroundColor:
|
|
userData.issues?.filter(
|
|
(iss: Issue) =>
|
|
iss.fields.status.name ===
|
|
'In Progress',
|
|
)?.length === 0
|
|
? 'yellow'
|
|
: '',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Box w='10%'>
|
|
<IconInnerShadowTopRightFilled
|
|
style={{ color: 'orange' }}
|
|
height={20}
|
|
width={20}
|
|
display={userData.issues?.filter(
|
|
(iss: Issue) =>
|
|
iss.fields.status.name === 'In Progress' &&
|
|
((Date.now() - (new Date(iss.changelog?.histories[0]?.created)).getTime()) > 172800000)
|
|
).length > 0 ? 'block' :'none'}
|
|
/>
|
|
</Box>
|
|
<Box display={'flex'}>
|
|
<Avatar
|
|
size={'sm'}
|
|
ml={'5px'}
|
|
src={userData.user.avatarUrls['48x48']}
|
|
/>
|
|
<Text ml={'md'} fw={600}>
|
|
{user}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
<Box
|
|
display={'flex'}
|
|
style={{
|
|
backgroundColor:
|
|
userData.p_total_spent >
|
|
userData.p_total_est
|
|
? '#ff960066'
|
|
: '',
|
|
justifyContent: 'center',
|
|
width: '20%',
|
|
}}
|
|
ml={'md'}
|
|
p="0 20px"
|
|
|
|
>
|
|
<Text ml={'md'} fw={700} fz={'sm'}>{`P: `}</Text>
|
|
<Text fw={700} c="green" fz={'sm'}>{`${
|
|
userData.p_total_spent / 60 / 60
|
|
}h/`}</Text>
|
|
<Text fw={700} c="blue" fz={'sm'}>{`${
|
|
userData.p_total_est / 60 / 60
|
|
}h`}</Text>
|
|
</Box>
|
|
<Box
|
|
display={'flex'}
|
|
style={{
|
|
backgroundColor:
|
|
data.users[user].total_spent >
|
|
data.users[user].total_est
|
|
? '#ff960066'
|
|
: '',
|
|
justifyContent: 'center',
|
|
width: '20%',
|
|
}}
|
|
ml={'md'}
|
|
p="0 20px"
|
|
>
|
|
<Text fw={700} fz={'sm'}>{`A: `}</Text>
|
|
<Text fw={700} c="green" fz={'sm'}>{`${
|
|
data.users[user].total_spent / 60 / 60
|
|
}h/`}</Text>
|
|
<Text fw={700} c="blue" fz={'sm'}>{`${
|
|
data.users[user].total_est / 60 / 60
|
|
}h`}</Text>
|
|
</Box>
|
|
</Box>
|
|
</Popover.Target>
|
|
<Popover.Dropdown>
|
|
<Text
|
|
size="xs"
|
|
style={{
|
|
maxHeight: '50vh',
|
|
overflow: 'auto',
|
|
}}
|
|
>
|
|
{userData.issues?.map((iss: Issue) => {
|
|
const date = new Date(iss.fields.updated)
|
|
const issStatus =
|
|
iss.fields.status.name === 'In Progress'
|
|
const issLastHistory =
|
|
Date.now() -
|
|
new Date(
|
|
iss.changelog?.histories[0]?.created,
|
|
).getTime()
|
|
return (
|
|
<Tooltip label="Click to view history">
|
|
<Box
|
|
className={
|
|
Date.now() - date.getTime() >
|
|
172800000 * 5
|
|
? classes['blinking-background']
|
|
: ''
|
|
}
|
|
style={{
|
|
margin: '10px 0',
|
|
padding: '5px',
|
|
borderRadius: '10px',
|
|
cursor: 'pointer',
|
|
backgroundColor: issStatus
|
|
? issLastHistory < 172800000
|
|
? '#d1f3d1'
|
|
: '#ffff8a'
|
|
: '',
|
|
}}
|
|
onClick={async () => {
|
|
setIssDetail(
|
|
getStateChanges(iss.changelog),
|
|
)
|
|
setOpened(true)
|
|
}}
|
|
>
|
|
<Text fw={600}>
|
|
<a
|
|
href={
|
|
'https://apactechvn.atlassian.net/browse/' +
|
|
iss.key
|
|
}
|
|
target="_blank"
|
|
>
|
|
{iss.key}
|
|
</a>
|
|
: {iss.fields.summary}
|
|
</Text>
|
|
<Text
|
|
fw={600}
|
|
ml={'50px'}
|
|
c={
|
|
iss.fields.status.statusCategory
|
|
.colorName
|
|
}
|
|
>
|
|
Status: {iss.fields.status.name}
|
|
</Text>
|
|
<Text fw={600} ml={'50px'}>
|
|
Time spent:{' '}
|
|
<Badge size="xs" bg={'orange'}>
|
|
{iss.fields.timespent / 60 / 60}
|
|
h
|
|
</Badge>
|
|
</Text>
|
|
<Text fw={600} ml={'50px'}>
|
|
EST:{' '}
|
|
<Badge size="xs">
|
|
{iss.fields
|
|
.timeoriginalestimate /
|
|
60 /
|
|
60}
|
|
h
|
|
</Badge>
|
|
</Text>
|
|
<Text fw={600} ml={'50px'}>
|
|
Updated:{' '}
|
|
<Badge size="xs">
|
|
{iss.fields.updated}
|
|
</Badge>
|
|
</Text>
|
|
</Box>
|
|
</Tooltip>
|
|
)
|
|
})}
|
|
</Text>
|
|
</Popover.Dropdown>
|
|
</Popover>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Card>
|
|
</Box>
|
|
)
|
|
})}
|
|
|
|
<Box
|
|
w={'32%'}
|
|
mt={'xl'}
|
|
style={{
|
|
display: data?.warningList?.length > 0 ? 'block' : 'none',
|
|
}}
|
|
>
|
|
<Card
|
|
shadow="md"
|
|
padding="xl"
|
|
component="a"
|
|
target="_blank"
|
|
style={{
|
|
minHeight: '28vh',
|
|
border: 'solid 1px rgb(28 74 115 / 64%)',
|
|
}}
|
|
>
|
|
<Card.Section
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '5px 20px',
|
|
backgroundColor: '#ffa200',
|
|
color: 'white',
|
|
}}
|
|
className={classes['blinking-background']}
|
|
>
|
|
<Avatar
|
|
size={'sm'}
|
|
src={
|
|
'https://cdn3.iconfinder.com/data/icons/toolbar-people/512/user_error_man_male_profile_warning-512.png'
|
|
}
|
|
/>
|
|
<Text ml={'md'} fw={700} size="lg">
|
|
WARNING - NO ASSIGNMENT
|
|
</Text>
|
|
</Card.Section>
|
|
|
|
{!loading &&
|
|
data?.warningList?.map((user: UserInfo) => {
|
|
return (
|
|
<Box style={{ cursor: 'pointer' }}>
|
|
<Box
|
|
mt={'sm'}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginLeft: '30px',
|
|
}}
|
|
>
|
|
<Avatar size={'sm'} src={user.avatarUrls['48x48']} />
|
|
<Text ml={'md'} fw={600} w={'40%'}>
|
|
{user.displayName}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
})}
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</div>
|
|
<Modal
|
|
title={<b>HISTORY</b>}
|
|
onClose={() => setOpened(false)}
|
|
opened={opened}
|
|
fullScreen
|
|
>
|
|
<pre>{issDetail}</pre>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Allocation
|