@@ -1,9 +1,10 @@
|
|||||||
import {createStructuredSelector} from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {useEffect, useState} from "react";
|
import {useCallback, useEffect, useState} from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {Card, Space, Table, Timeline} from "antd";
|
import {Card, Space, Table, Timeline} from "antd";
|
||||||
|
import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -12,6 +13,9 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
|
||||||
|
|
||||||
|
|
||||||
export function JobLifecycleComponent({bodyshop, job, ...rest}) {
|
export function JobLifecycleComponent({bodyshop, job, ...rest}) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lifecycleData, setLifecycleData] = useState(null);
|
const [lifecycleData, setLifecycleData] = useState(null);
|
||||||
@@ -24,8 +28,8 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) {
|
|||||||
const response = await axios.post("/job/lifecycle", {
|
const response = await axios.post("/job/lifecycle", {
|
||||||
jobids: job.id,
|
jobids: job.id,
|
||||||
});
|
});
|
||||||
console.dir(response.data.data.transitions, {depth: null});
|
const data = response.data.transition[job.id];
|
||||||
setLifecycleData(response.data.data.transitions);
|
setLifecycleData(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,11 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) {
|
|||||||
});
|
});
|
||||||
}, [job]);
|
}, [job]);
|
||||||
|
|
||||||
|
// // TODO - Delete this useEffect, it is for testing
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.dir(lifecycleData)
|
||||||
|
// }, [lifecycleData]);
|
||||||
|
|
||||||
const columnKeys = [
|
const columnKeys = [
|
||||||
'start',
|
'start',
|
||||||
'end',
|
'end',
|
||||||
@@ -54,23 +63,82 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) {
|
|||||||
key: key,
|
key: key,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of cells for the Pie Chart
|
||||||
|
* @type {function(): *[]}
|
||||||
|
*/
|
||||||
|
const renderCells = useCallback(() => {
|
||||||
|
const entires = Object
|
||||||
|
.entries(lifecycleData.durations)
|
||||||
|
.filter(([name, value]) => {
|
||||||
|
return value !== 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
return entires.map(([name, value], index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]}>
|
||||||
|
<LabelList dataKey="name" position="insideTop" />
|
||||||
|
<LabelList dataKey="value" position="insideBottom" />
|
||||||
|
</Cell>
|
||||||
|
));
|
||||||
|
}, [lifecycleData, job]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of objects with the name and value of the duration
|
||||||
|
* @type {function(): {name: *, value}[]}
|
||||||
|
*/
|
||||||
|
const durationsData = useCallback(() => {
|
||||||
|
return Object.entries(lifecycleData.durations) .filter(([name, value]) => {
|
||||||
|
return value !== 0;
|
||||||
|
}).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value: value / 1000
|
||||||
|
}))
|
||||||
|
}, [lifecycleData, job]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card loading={loading} title='Job Lifecycle Component'>
|
<Card loading={loading} title='Job Lifecycle Component'>
|
||||||
{!loading ? (
|
{!loading ? (
|
||||||
<Space direction='vertical' style={{width: '100%'}}>
|
lifecycleData ? (
|
||||||
<Card type='inner' title='Table Format'>
|
<Space direction='vertical' style={{width: '100%'}}>
|
||||||
<Table columns={columns} dataSource={lifecycleData} />
|
<Card type='inner' title='Table Format'>
|
||||||
|
<Table columns={columns} dataSource={lifecycleData.lifecycle}/>
|
||||||
|
</Card>
|
||||||
|
<Space direction='horizontal' style={{width: '100%'}} align='start'>
|
||||||
|
<Card type='inner' title='Timeline Format'>
|
||||||
|
<Timeline>
|
||||||
|
{lifecycleData.lifecycle.map((item, index) => (
|
||||||
|
<Timeline.Item key={index} color={item.value === 'Open' ? 'green' : item.value === 'Scheduled' ? 'yellow' : 'red'}>
|
||||||
|
{item.value} - {new Date(item.start).toLocaleString()}
|
||||||
|
</Timeline.Item>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
</Card>
|
||||||
|
<Card type='inner' title='Durations'>
|
||||||
|
<PieChart width={400} height={400}>
|
||||||
|
<Pie
|
||||||
|
data={durationsData()}
|
||||||
|
cx={200}
|
||||||
|
cy={200}
|
||||||
|
labelLine={false}
|
||||||
|
label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{renderCells()}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip/>
|
||||||
|
<Legend/>
|
||||||
|
</PieChart>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Card type='inner' style={{textAlign: 'center'}}>
|
||||||
|
There is currently no lifecycle data for this job.
|
||||||
</Card>
|
</Card>
|
||||||
<Card type='inner' title='Timeline Format'>
|
)
|
||||||
<Timeline>
|
|
||||||
{lifecycleData.map((item, index) => (
|
|
||||||
<Timeline.Item key={index} color={item.value === 'Open' ? 'green' : item.value === 'Scheduled' ? 'yellow' : 'red'}>
|
|
||||||
{item.value} - {new Date(item.start).toLocaleString()}
|
|
||||||
</Timeline.Item>
|
|
||||||
))}
|
|
||||||
</Timeline>
|
|
||||||
</Card>
|
|
||||||
</Space>
|
|
||||||
) : (
|
) : (
|
||||||
<Card type='inner' title='Loading'>
|
<Card type='inner' title='Loading'>
|
||||||
Loading Job Timelines....
|
Loading Job Timelines....
|
||||||
|
|||||||
@@ -518,8 +518,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: uuid!) {
|
exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) {
|
||||||
transitions(where: {jobid: {_eq: $jobid}}, order_by: {id: asc}) {
|
transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) {
|
||||||
start
|
start
|
||||||
end
|
end
|
||||||
value
|
value
|
||||||
@@ -529,6 +529,7 @@ exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: u
|
|||||||
type
|
type
|
||||||
created_at
|
created_at
|
||||||
updated_at
|
updated_at
|
||||||
|
jobid
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,73 @@
|
|||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const jobLifecycle = (req, res) => {
|
|
||||||
|
const calculateStatusDuration = (transitions) => {
|
||||||
|
let statusDuration = {};
|
||||||
|
|
||||||
|
transitions.forEach((transition, index) => {
|
||||||
|
let duration = transition.duration;
|
||||||
|
|
||||||
|
// If there is no prev_value, it is the first transition
|
||||||
|
if (!transition.prev_value) {
|
||||||
|
statusDuration[transition.value] = duration;
|
||||||
|
}
|
||||||
|
// If there is no next_value, it is the last transition (the active one)
|
||||||
|
else if (!transition.next_value) {
|
||||||
|
if (statusDuration[transition.value]) {
|
||||||
|
statusDuration[transition.value] += duration;
|
||||||
|
} else {
|
||||||
|
statusDuration[transition.value] = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For all other transitions
|
||||||
|
else {
|
||||||
|
if (statusDuration[transition.value]) {
|
||||||
|
statusDuration[transition.value] += duration;
|
||||||
|
} else {
|
||||||
|
statusDuration[transition.value] = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return statusDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const jobLifecycle = async (req, res) => {
|
||||||
const {jobids} = req.body;
|
const {jobids} = req.body;
|
||||||
|
|
||||||
return _.isArray(jobids) ?
|
const jobIDs = _.isArray(jobids) ? jobids : [jobids];
|
||||||
handleMultipleJobs(jobids, req, res) :
|
|
||||||
handleSingleJob(jobids, req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMultipleJobs = (jobIDs, req, res) => {
|
|
||||||
return res.status(200).send(jobIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSingleJob = async (jobIds, req, res) => {
|
|
||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,});
|
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,});
|
||||||
|
|
||||||
|
const transitions = resp.transitions;
|
||||||
|
|
||||||
|
if (!transitions) {
|
||||||
|
return res.status(200).json({
|
||||||
|
jobIDs,
|
||||||
|
transitions: []
|
||||||
|
});
|
||||||
|
|
||||||
const response = {
|
|
||||||
jobIds,
|
|
||||||
data: resp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(response);
|
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
|
||||||
|
|
||||||
|
const groupedTransitions = {};
|
||||||
|
|
||||||
|
for (let jobId in transitionsByJobId) {
|
||||||
|
groupedTransitions[jobId] = {
|
||||||
|
lifecycle: transitionsByJobId[jobId],
|
||||||
|
durations: calculateStatusDuration(transitionsByJobId[jobId])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
jobIDs,
|
||||||
|
transition: groupedTransitions,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = jobLifecycle;
|
module.exports = jobLifecycle;
|
||||||
Reference in New Issue
Block a user