Create a workflow statistics page from Jira

This commit is contained in:
JOSEPH LE 2024-05-20 16:30:57 +07:00
parent 010fe6e650
commit 9b630fe1e1
3 changed files with 320 additions and 342 deletions

View File

@ -76,7 +76,7 @@ class JiraService
"assignee = '%s' AND status IN ('backlog', 'todo', 'in progress')", "assignee = '%s' AND status IN ('backlog', 'todo', 'in progress')",
$accountId $accountId
), ),
'maxResults' => 10, 'maxResults' => 50,
'startAt' => $startAt 'startAt' => $startAt
]; ];
@ -104,7 +104,7 @@ class JiraService
$body = [ $body = [
'jql' => "worklogAuthor = '{$accountId}'AND worklogDate >= '{$startDate}' AND worklogDate <= '{$endDate}'", 'jql' => "worklogAuthor = '{$accountId}'AND worklogDate >= '{$startDate}' AND worklogDate <= '{$endDate}'",
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'project'], 'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'project'],
'maxResults' => 50 'maxResults' => 100
]; ];
$response = $this->client->post('/rest/api/3/search', [ $response = $this->client->post('/rest/api/3/search', [
@ -134,7 +134,7 @@ class JiraService
] ]
])->then(function ($checkApiResponse) use ($issueId, $index) { ])->then(function ($checkApiResponse) use ($issueId, $index) {
$checkApi = json_decode($checkApiResponse->getBody()->getContents(), true); $checkApi = json_decode($checkApiResponse->getBody()->getContents(), true);
$maxResults = 50; $maxResults = 100;
$totalWorklogs = $checkApi['total']; $totalWorklogs = $checkApi['total'];
return $this->client->getAsync("/rest/api/3/issue/{$issueId}/worklog", [ return $this->client->getAsync("/rest/api/3/issue/{$issueId}/worklog", [
'query' => [ 'query' => [

View File

@ -116,8 +116,13 @@ const Tracking = () => {
} }
useEffect(()=>{ useEffect(()=>{
setInterval(()=>{ if(listTracking.data.length === 0){
getAllTracking() getAllTracking()
}
setInterval(()=>{
if(window.location.pathname.includes('tracking')){
getAllTracking()
}
}, 7000) }, 7000)
}, []) }, [])
return ( return (

View File

@ -117,7 +117,7 @@ const Worklogs = () => {
startDate: startDate:
localStorage.getItem('data') !== null localStorage.getItem('data') !== null
? JSON.parse(localStorage.getItem('data')!).date.startDate ? JSON.parse(localStorage.getItem('data')!).date.startDate
: moment(Date.now()).format('YYYY-MM-DD'), : moment(Date.now()-604800000).format('YYYY-MM-DD'),
endDate: endDate:
localStorage.getItem('data') !== null localStorage.getItem('data') !== null
? JSON.parse(localStorage.getItem('data')!).date.endDate ? JSON.parse(localStorage.getItem('data')!).date.endDate
@ -172,65 +172,88 @@ const Worklogs = () => {
</Box> </Box>
) : ( ) : (
<div style={{ display: 'flex', flexFlow: 'column' }}> <div style={{ display: 'flex', flexFlow: 'column' }}>
<div className={classes.title}> <div className={classes.title}>
<h3> <h3>
<Text>Admin/</Text>Worklogs <Text>Admin/</Text>Worklogs
{!updating ? ( {!updating ? (
<Text fs={'italic'} fz={'xs'} c={'gray'}> <Text fs={'italic'} fz={'xs'} c={'gray'}>
Updating data in the background ... Updating data in the background ...
</Text> </Text>
) : ( ) : (
'' ''
)} )}
</h3> </h3>
</div> </div>
<Box <Box
display={'flex'} display={'flex'}
w={'30%'} style={{
style={{ flexFlow: 'column',
float: 'right', }}
// margin: '10px', >
alignItems: 'end', <Text fz={12} fs={'italic'} c={'red'} fw={600} mt={'md'}>
justifyContent: 'space-between', {`* The current data is from ${JSON.parse(localStorage.getItem('data')!).date.startDate} to ${JSON.parse(localStorage.getItem('data')!).date.endDate}`}
}} </Text>
> <Text fz={12} fs={'italic'} c={'red'} fw={600}>
<DateInput {`* If you need data outside this time period, please select a date and click "Search" to update the latest data.`}
size="xs" </Text>
label="From date:" <Box w={'100%'} display={'flex'} style={{ flexFlow: 'column' }}>
value={new Date(date.startDate)} <Box
w={'40%'} w={'100%'}
clearable display={'flex'}
onChange={(e) => { style={{
setDate({ ...date, startDate: moment(e).format('YYYY-MM-DD') }) alignItems: 'end',
}} // justifyContent: 'space-between',
/> flexWrap: 'wrap',
<DateInput
size="xs"
label="To date:"
value={new Date(date.endDate)}
clearable
w={'40%'}
onChange={(e) => {
setDate({ ...date, endDate: moment(e).format('YYYY-MM-DD') })
}}
/>
<Button
size="xs"
onClick={() => {
getAllWorklogs()
}} }}
> >
Search <DateInput
</Button> size="xs"
label="From date:"
value={new Date(date.startDate)}
w={'20%'}
clearable
onChange={(e) => {
setDate({ ...date, startDate: moment(e).format('YYYY-MM-DD') });
}}
/>
<DateInput
size="xs"
label="To date:"
value={new Date(date.endDate)}
clearable
m={"0 10px"}
w={'20%'}
onChange={(e) => {
setDate({ ...date, endDate: moment(e).format('YYYY-MM-DD') });
}}
/>
<Button
size="xs"
onClick={() => {
getAllWorklogs();
}}
>
Search
</Button>
</Box>
</Box> </Box>
{/* Box main content */} </Box>
<Box display={'flex'} mt={'lg'}> {/* Box main content */}
<Box w={'10%'} > <Box display={'flex'} mt={'lg'} style={{ flexWrap: 'wrap' }}>
<Text fw={700} fz={14}> <Box w={{ base: '100%', md: '15%' }} mb={'lg'}>
Members <Text fw={700} fz={14}>
</Text> Members
<Box style={{border:"solid 1px gray", padding:"10px", marginRight:"10px", borderRadius:"5px", boxShadow: '1px 1px 5px 1px gray',}}> </Text>
{worklogs.map((w) => ( <Box
style={{
border: 'solid 1px gray',
padding: '10px 20px',
marginRight: '10px',
borderRadius: '5px',
boxShadow: '1px 1px 5px 1px gray',
}}
>
{worklogs.map((w) => (
<Box <Box
key={w.username} key={w.username}
style={{ style={{
@ -251,309 +274,259 @@ const Worklogs = () => {
</a> </a>
</Box> </Box>
))} ))}
</Box>
</Box> </Box>
<Box </Box>
w={'90%'} <Box
h={'85vh'} w={{ base: '100%', md: '85%' }}
display={'flex'} h={'85vh'}
style={{ overflowX: 'auto', flexFlow: 'column' }} display={'flex'}
> style={{ overflowX: 'auto', flexFlow: 'column' }}
{worklogs?.map((user, index) => ( >
// Box user {worklogs?.map((user, index) => (
<Box // Box user
id={user.username} <Box
key={index} id={user.username}
p={'sm'} key={index}
style={{ p={'sm'}
border: 'solid 1px gray', style={{
borderRadius: '5px', border: 'solid 1px gray',
borderColor: '#afafaf', borderRadius: '5px',
marginBottom: '10px', borderColor: '#afafaf',
backgroundColor: marginBottom: '10px',
index % 2 === 0 ? 'rgb(201 201 201 / 28%)' : 'white', backgroundColor: index % 2 === 0 ? 'rgb(201 201 201 / 28%)' : 'white',
}} }}
> >
<Text ta={'left'} mb={'xs'} fw={800} display={'flex'}> <Text ta={'left'} mb={'xs'} fw={800} display={'flex'}>
<Avatar <Avatar src={user.user.avatarUrls['48x48']} size={'sm'} m={'0 5px'} />
src={user.user.avatarUrls['48x48']} {user.username}(
size={'sm'} {user?.information.issues.reduce((total, issue) => {
m={'0 5px'} const totalSpent = issue.fields.worklog.worklogs?.reduce(
/> (accumulator, currentValue) => {
{user.username}( if (
{user?.information.issues.reduce( parseInt(moment(date.startDate).format('YYYYMMDD')) <=
(total: number, issue: Issue) => { parseInt(moment(currentValue.started).format('YYYYMMDD')) &&
var totalSpent = issue.fields.worklog.worklogs?.reduce( parseInt(moment(currentValue.started).format('YYYYMMDD')) <=
(accumulator: number, currentValue: WorkLog) => { parseInt(moment(date.endDate).format('YYYYMMDD')) &&
if ( currentValue.updateAuthor.displayName === user.username
parseInt(moment(date.startDate).format('YYYYMMDD')) <= ) {
parseInt( return accumulator + currentValue.timeSpentSeconds;
moment(currentValue.started).format('YYYYMMDD'), }
) && return accumulator;
parseInt(
moment(currentValue.started).format('YYYYMMDD'),
) <=
parseInt(moment(date.endDate).format('YYYYMMDD')) &&
currentValue.updateAuthor.displayName ===
user.username
) {
return accumulator + currentValue.timeSpentSeconds
}
return accumulator
},
0,
)
return total + totalSpent
}, },
0, 0,
) / );
60 / return total + totalSpent;
60} }, 0) /
h) 60 /
</Text> 60}
{/* Box issue-todo */} h)
<Box display={'flex'} style={{ justifyContent: 'space-between' }}> </Text>
{/* Box issue */} {/* Box issue-todo */}
<Box <Box display={'flex'} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
w={'50%'} {/* Box issue */}
style={{ <Box
border: 'solid 1px gray', w={{ base: '100%', md: '50%' }}
borderRadius: '5px', style={{
padding: '10px', border: 'solid 1px gray',
marginBottom: '5px', borderRadius: '5px',
overflowX: 'hidden', padding: '10px',
color: '#412d2d', marginBottom: '5px',
backgroundColor: '#d1cdce', overflowX: 'hidden',
}} color: '#412d2d',
> backgroundColor: '#d1cdce',
<Text fw={700} ta={'center'} mb={'5px'}> }}
WORKLOG >
</Text> <Text fw={700} ta={'center'} mb={'5px'}>
{user.information.issues.map((iss) => { WORKLOG
if ( </Text>
iss.fields.worklog.worklogs.filter( {user.information.issues.map((iss) => {
(w) => if (
parseInt(moment(date.startDate).format('YYYYMMDD')) <= iss.fields.worklog.worklogs.filter(
parseInt(moment(w.started).format('YYYYMMDD')) && (w) =>
parseInt(moment(date.endDate).format('YYYYMMDD')) >= parseInt(moment(date.startDate).format('YYYYMMDD')) <=
parseInt(moment(w.started).format('YYYYMMDD')) && parseInt(moment(w.started).format('YYYYMMDD')) &&
w.updateAuthor.displayName === user.username, parseInt(moment(date.endDate).format('YYYYMMDD')) >=
).length > 0 parseInt(moment(w.started).format('YYYYMMDD')) &&
) { w.updateAuthor.displayName === user.username,
return ( ).length > 0
<Box ) {
key={iss.id}
style={{
border: 'solid 1px gray',
borderRadius: '5px',
boxShadow: '1px 1px 5px 1px gray',
padding: '10px',
marginBottom: '8px',
maxHeight: '90vh',
overflowX: 'hidden',
color: '#412d2d',
backgroundColor: 'white',
}}
>
{/* Box information issue */}
<Box>
<Text
fz={14}
display={'flex'}
style={{ alignItems: 'start' }}
>
<b>Summary:</b>{' '}
<Box
display={'flex'}
style={{ alignItems: 'center' }}
>
<Avatar
src={iss.fields.project?.avatarUrls['16x16']}
size={'xs'}
m={'0 5px'}
/>
<Text fz={14} mr={'xs'}>
{iss.fields.project.name}
</Text>
</Box>
<a
href={`https://apactechvn.atlassian.net/browse/${iss.key}`}
target="_blank"
>
{iss.fields.summary}
</a>
</Text>
<Text fz={14}>
<b>Estimate:</b>{' '}
{iss.fields.timeoriginalestimate / 60 / 60}h
</Text>
<Text fz={14}>
<b>Total time spent:</b>{' '}
{iss.fields.timespent / 60 / 60}h
</Text>
<Text fz={14}>
<b>Time spent <span style={{fontSize:"11px"}}>({date.startDate === date.endDate ? date.startDate : date.startDate+" to "+date.endDate})</span>:</b>
{' '}{iss.fields.worklog.worklogs?.reduce(
(
accumulator: number,
currentValue: WorkLog,
) => {
if (
parseInt(
moment(date.startDate).format('YYYYMMDD'),
) <=
parseInt(
moment(currentValue.started).format(
'YYYYMMDD',
),
) &&
parseInt(
moment(currentValue.started).format(
'YYYYMMDD',
),
) <=
parseInt(
moment(date.endDate).format('YYYYMMDD'),
) &&
currentValue.updateAuthor.displayName ===
user.username
) {
return (
accumulator +
currentValue.timeSpentSeconds
)
}
return accumulator
},
0,
) /
60 /
60}
h
</Text>
</Box>
{iss.fields.worklog.worklogs?.map((log, index) => {
if (
moment(date.startDate).format('YYYYMMDD') <=
moment(log.started).format('YYYYMMDD') &&
moment(log.started).format('YYYYMMDD') <=
moment(date.endDate).format('YYYYMMDD') &&
log.updateAuthor.displayName === user.username
) {
return (
// Box worklog
<Box
key={index}
style={{
padding: '4px 8px',
marginBottom: '5px',
marginLeft: '10px',
backgroundColor: '#d9d9d9',
}}
>
<Text fz={13}>
<b>Start date:</b>{' '}
{moment(log.started).format(
'HH:mm YYYY/MM/DD',
)}
</Text>
<Text fz={13}>
<b>Time spent:</b> {log.timeSpent}
</Text>
{log?.comment &&
log?.comment?.content[0]?.content[0]
?.text && (
<Text fz={13}>
<b>Comment:</b>{' '}
{log?.comment &&
log?.comment?.content[0]?.content[0]
?.text}
</Text>
)}
</Box>
)
}
})}
</Box>
)
}
})}
</Box>
{/* Box todo */}
<Box
w={'49%'}
style={{
border: 'solid 1px gray',
borderRadius: '5px',
padding: '10px',
marginBottom: '5px',
overflowX: 'hidden',
color: '#412d2d',
backgroundColor: '#f9ffa47a',
}}
>
<Text fw={700} ta={'center'} mb={'5px'}>
ASSIGNMENT
</Text>
{user.tasksAssign.issues?.map((iss, index) => {
return ( return (
<Box <Box
key={index} key={iss.id}
style={{ style={{
border: 'solid 1px gray', border: 'solid 1px gray',
borderRadius: '5px', borderRadius: '5px',
boxShadow: '1px 1px 5px 1px gray', boxShadow: '1px 1px 5px 1px gray',
padding: '10px', padding: '10px',
marginBottom: '5px', marginBottom: '8px',
maxHeight: '90vh',
overflowX: 'hidden', overflowX: 'hidden',
color: '#412d2d', color: '#412d2d',
backgroundColor: 'white', backgroundColor: 'white',
}} }}
> >
<Box display={'flex'} style={{ alignItems: 'center' }}> {/* Box information issue */}
<Avatar <Box>
src={iss.fields.project?.avatarUrls['16x16']} <Text fz={14} c={iss.fields.status.statusCategory.colorName}>
size={'xs'} <b>{iss.fields.status.name}</b>
m={'0 5px 0 0'} </Text>
/> <Text fz={14} display={'flex'} style={{ alignItems: 'start' }}>
<Text fz={14} mr={'xs'}> <b>Summary:</b>{' '}
{iss.fields.project.name} <Box display={'flex'} style={{ alignItems: 'center' }}>
<Avatar
src={iss.fields.project?.avatarUrls['16x16']}
size={'xs'}
m={'0 5px'}
/>
<Text fz={14} mr={'xs'}>
{iss.fields.project.name}
</Text>
</Box>
<a href={`https://apactechvn.atlassian.net/browse/${iss.key}`} target="_blank">
{iss.fields.summary}
</a>
</Text>
<Text fz={14}>
<b>Estimate:</b> {iss.fields.timeoriginalestimate / 60 / 60}h
</Text>
<Text fz={14}>
<b>Total time spent:</b> {iss.fields.timespent / 60 / 60}h
</Text>
<Text fz={14}>
<b>
Time spent{' '}
<span style={{ fontSize: '11px' }}>
(
{date.startDate === date.endDate
? date.startDate
: date.startDate + ' to ' + date.endDate}
)
</span>
:
</b>{' '}
{iss.fields.worklog.worklogs?.reduce(
(accumulator, currentValue) => {
if (
parseInt(moment(date.startDate).format('YYYYMMDD')) <=
parseInt(moment(currentValue.started).format('YYYYMMDD')) &&
parseInt(moment(currentValue.started).format('YYYYMMDD')) <=
parseInt(moment(date.endDate).format('YYYYMMDD')) &&
currentValue.updateAuthor.displayName === user.username
) {
return accumulator + currentValue.timeSpentSeconds;
}
return accumulator;
},
0,
) /
60 /
60}
h
</Text> </Text>
</Box> </Box>
<Text {iss.fields.worklog.worklogs?.map((log, index) => {
fz={14} if (
display={'flex'} moment(date.startDate).format('YYYYMMDD') <= moment(log.started).format('YYYYMMDD') &&
style={{ alignItems: 'start' }} moment(log.started).format('YYYYMMDD') <= moment(date.endDate).format('YYYYMMDD') &&
> log.updateAuthor.displayName === user.username
<b style={{ marginRight: '5px' }}>Summary:</b> ) {
<a return (
href={`https://apactechvn.atlassian.net/browse/${iss.key}`} // Box worklog
target="_blank" <Box
> key={index}
{iss.fields.summary} style={{
</a> padding: '4px 8px',
</Text> marginBottom: '5px',
<Text fz={13}> marginLeft: '10px',
<b>Time spent:</b> {iss.fields.timespent / 60 / 60}h backgroundColor: '#d9d9d9',
</Text> }}
<Text fz={13}> >
<b>Estimate:</b> {iss.fields.timeoriginalestimate / 60 / 60}h <Text fz={13}>
</Text> <b>Start date:</b> {moment(log.started).format('HH:mm YYYY/MM/DD')}
<Text fz={'14'}> </Text>
<b style={{ marginRight: '5px' }}>Status:</b> <Text fz={13}>
{iss.fields.status.name} <b>Time spent:</b> {log.timeSpent}
</Text>
{log?.comment && log?.comment?.content[0]?.content[0]?.text && (
<Text fz={13}>
<b>Comment:</b> {log?.comment?.content[0]?.content[0]?.text}
</Text>
)}
</Box>
);
}
})}
</Box>
);
}
})}
</Box>
{/* Box todo */}
<Box
w={{ base: '100%', md: '49%' }}
style={{
border: 'solid 1px gray',
borderRadius: '5px',
padding: '10px',
marginBottom: '5px',
overflowX: 'hidden',
color: '#412d2d',
backgroundColor: '#f9ffa47a',
}}
>
<Text fw={700} ta={'center'} mb={'5px'}>
ASSIGNMENT
</Text>
{user.tasksAssign.issues?.map((iss, index) => {
return (
<Box
key={index}
style={{
border: 'solid 1px gray',
borderRadius: '5px',
boxShadow: '1px 1px 5px 1px gray',
padding: '10px',
marginBottom: '5px',
overflowX: 'hidden',
color: '#412d2d',
backgroundColor: 'white',
}}
>
<Box display={'flex'} style={{ alignItems: 'center' }}>
<Avatar
src={iss.fields.project?.avatarUrls['16x16']}
size={'xs'}
m={'0 5px 0 0'}
/>
<Text fz={14} mr={'xs'}>
{iss.fields.project.name}
</Text> </Text>
</Box> </Box>
) <Text fz={14} display={'flex'} style={{ alignItems: 'start' }}>
})} <b style={{ marginRight: '5px' }}>Summary:</b>
</Box> <a href={`https://apactechvn.atlassian.net/browse/${iss.key}`} target="_blank">
{iss.fields.summary}
</a>
</Text>
<Text fz={13}>
<b>Time spent:</b> {iss.fields.timespent / 60 / 60}h
</Text>
<Text fz={13}>
<b>Estimate:</b> {iss.fields.timeoriginalestimate / 60 / 60}h
</Text>
<Text fz={14} c={iss.fields.status.statusCategory.colorName}>
<b>{iss.fields.status.name}</b>
</Text>
</Box>
);
})}
</Box> </Box>
</Box> </Box>
))} </Box>
</Box> ))}
</Box> </Box>
</div> </Box>
</div>
) )
} }