Merge branch 'master-beta' into master-AIO
This commit is contained in:
@@ -14247,6 +14247,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>surveycompletesubtitle</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>surveycompletetitle</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
@@ -14294,11 +14336,116 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>surveyid</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>validuntil</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
<name>labels</name>
|
<name>labels</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>copyright</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>greeting</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>intro</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>nologgedinuser</name>
|
<name>nologgedinuser</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -20620,6 +20767,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>human_readable</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>percentage</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>relative_end</name>
|
<name>relative_end</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -20683,6 +20872,48 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>status</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>status_count</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>value</name>
|
<name>value</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -20709,6 +20940,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>content</name>
|
<name>content</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>calculated_based_on</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>current_status_accumulated_time</name>
|
<name>current_status_accumulated_time</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -20751,6 +21003,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>jobs_in_since</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>legend_title</name>
|
<name>legend_title</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -20947,6 +21220,53 @@
|
|||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>titles</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>dashboard</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>top_durations</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
<folder_node>
|
<folder_node>
|
||||||
@@ -32538,6 +32858,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>labor_hrs</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>labor_rates_subtotal</name>
|
<name>labor_rates_subtotal</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -42719,6 +43060,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>job_lifecycle_ro</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>job_notes</name>
|
<name>job_notes</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {Card, Table, Tag} from "antd";
|
||||||
|
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import dayjs from '../../../utils/day';
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const fortyFiveDaysAgo = () => dayjs().subtract(45, 'day').toLocaleString();
|
||||||
|
|
||||||
|
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [lifecycleData, setLifecycleData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getLifecycleData() {
|
||||||
|
if (data && data.job_lifecycle) {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.post("/job/lifecycle", {
|
||||||
|
jobids: data.job_lifecycle.map(x => x.id),
|
||||||
|
statuses: bodyshop.md_order_statuses
|
||||||
|
});
|
||||||
|
setLifecycleData(response.data.durations);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLifecycleData().catch(e => {
|
||||||
|
console.error(`Error in getLifecycleData: ${e}`);
|
||||||
|
})
|
||||||
|
}, [data, bodyshop]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('job_lifecycle.columns.status'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
bgColor: 'red',
|
||||||
|
key: 'status',
|
||||||
|
render: (text, record) => {
|
||||||
|
return <Tag color={record.color}>{record.status}</Tag>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('job_lifecycle.columns.human_readable'),
|
||||||
|
dataIndex: 'humanReadable',
|
||||||
|
key: 'humanReadable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('job_lifecycle.columns.status_count'),
|
||||||
|
key: 'statusCount',
|
||||||
|
render: (text, record) => {
|
||||||
|
return lifecycleData.statusCounts[record.status];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('job_lifecycle.columns.percentage'),
|
||||||
|
dataIndex: 'percentage',
|
||||||
|
key: 'percentage',
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.percentage.toFixed(2) + '%';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
|
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
|
||||||
|
<LoadingSkeleton loading={loading}>
|
||||||
|
<div style={{overflow: 'scroll', height: "100%"}}>
|
||||||
|
<div id="bar-container" style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100px',
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: '5px',
|
||||||
|
borderWidth: '5px',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: '#f0f2f5',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0
|
||||||
|
}}>
|
||||||
|
{lifecycleData.summations.map((key, index, array) => {
|
||||||
|
const isFirst = index === 0;
|
||||||
|
const isLast = index === array.length - 1;
|
||||||
|
return (
|
||||||
|
<div key={key.status} style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
|
||||||
|
borderTop: '1px solid #f0f2f5',
|
||||||
|
borderBottom: '1px solid #f0f2f5',
|
||||||
|
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
|
||||||
|
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
|
||||||
|
|
||||||
|
backgroundColor: key.color,
|
||||||
|
width: `${key.percentage}%`
|
||||||
|
}}
|
||||||
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
|
>
|
||||||
|
|
||||||
|
{key.percentage > 15 ?
|
||||||
|
<>
|
||||||
|
<div>{key.roundedPercentage}</div>
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#f0f2f5',
|
||||||
|
borderRadius: '5px',
|
||||||
|
paddingRight: '2px',
|
||||||
|
paddingLeft: '2px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
}}>
|
||||||
|
{key.status}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
|
||||||
|
style={{marginTop: '10px'}}>
|
||||||
|
<div>
|
||||||
|
{lifecycleData.summations.map((key) => (
|
||||||
|
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
|
||||||
|
<div
|
||||||
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#f0f2f5',
|
||||||
|
color: '#000',
|
||||||
|
padding: '4px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
|
||||||
|
<Table size="small" pagination={false} columns={columns}
|
||||||
|
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</LoadingSkeleton>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JobLifecycleDashboardGQL = `
|
||||||
|
job_lifecycle: jobs(where: {
|
||||||
|
actual_in: {
|
||||||
|
_gte: "${dayjs().subtract(45, 'day').toISOString()}"
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
actual_in
|
||||||
|
} `;
|
||||||
@@ -42,6 +42,9 @@ import DashboardScheduledInToday, {
|
|||||||
import DashboardScheduledOutToday, {
|
import DashboardScheduledOutToday, {
|
||||||
DashboardScheduledOutTodayGql,
|
DashboardScheduledOutTodayGql,
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
||||||
|
import JobLifecycleDashboardComponent, {
|
||||||
|
JobLifecycleDashboardGQL
|
||||||
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
|
||||||
import "./dashboard-grid.styles.scss";
|
import "./dashboard-grid.styles.scss";
|
||||||
import {GenerateDashboardData} from "./dashboard-grid.utils";
|
import {GenerateDashboardData} from "./dashboard-grid.utils";
|
||||||
|
|
||||||
@@ -260,6 +263,7 @@ const componentList = {
|
|||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 2,
|
||||||
},
|
},
|
||||||
|
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
||||||
MonthlyEmployeeEfficency: {
|
MonthlyEmployeeEfficency: {
|
||||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||||
component: DashboardMonthlyEmployeeEfficiency,
|
component: DashboardMonthlyEmployeeEfficiency,
|
||||||
@@ -287,6 +291,15 @@ const componentList = {
|
|||||||
w: 10,
|
w: 10,
|
||||||
h: 3,
|
h: 3,
|
||||||
},
|
},
|
||||||
|
JobLifecycle: {
|
||||||
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||||
|
component: JobLifecycleDashboardComponent,
|
||||||
|
gqlFragment: JobLifecycleDashboardGQL,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDashboardQuery = (state) => {
|
const createDashboardQuery = (state) => {
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({loading, data}) {
|
|||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
className="imex-flex-row__margin-large"
|
className="imex-flex-row__margin-large"
|
||||||
title={t("jobs.fields.ded_amt")}
|
title={t("jobs.fields.customerowing")}
|
||||||
value={Dinero({
|
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
|
||||||
amount: Math.round((data.ded_amt || 0) * 100),
|
|
||||||
}).toFormat()}
|
|
||||||
/>
|
/>
|
||||||
<Statistic
|
<Statistic
|
||||||
className="imex-flex-row__margin-large"
|
className="imex-flex-row__margin-large"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled,} from "@ant-design/icons";
|
import {BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled,} from "@ant-design/icons";
|
||||||
import {Card, Col, Row, Space, Tag, Tooltip} from "antd";
|
import {Card, Col, Divider, Row, Space, Tag, Tooltip} from "antd";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
@@ -22,6 +22,7 @@ import ProductionListColumnProductionNote
|
|||||||
from "../production-list-columns/production-list-columns.productionnote.component";
|
from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import "./jobs-detail-header.styles.scss";
|
import "./jobs-detail-header.styles.scss";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -58,6 +59,13 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
|
|||||||
${job.v_make_desc || ""}
|
${job.v_make_desc || ""}
|
||||||
${job.v_model_desc || ""}`.trim();
|
${job.v_model_desc || ""}`.trim();
|
||||||
|
|
||||||
|
const bodyHrs = job.joblines
|
||||||
|
.filter((j) => j.mod_lbr_ty !== "LAR")
|
||||||
|
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
|
const refinishHrs = job.joblines
|
||||||
|
.filter((line) => line.mod_lbr_ty === "LAR")
|
||||||
|
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||||
|
|
||||||
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
const ownerTitle = OwnerNameDisplayFunction(job).trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,7 +97,9 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
|
|||||||
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
|
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
|
||||||
job.scheduled_in ? (
|
job.scheduled_in ? (
|
||||||
<Tag>
|
<Tag>
|
||||||
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format('YYYY-MM-DD')}`}>
|
||||||
|
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
||||||
|
</Link>
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -295,6 +305,11 @@ export function JobsDetailHeader({job, bodyshop, disabled}) {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<JobEmployeeAssignments job={job}/>
|
<JobEmployeeAssignments job={job}/>
|
||||||
|
<Divider style={{ margin: ".5rem" }} />
|
||||||
|
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
||||||
|
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
|
||||||
|
{(bodyHrs + refinishHrs).toFixed(1)}
|
||||||
|
</DataLabel>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({bodyshop, owner}) {
|
|||||||
title: t("jobs.fields.vehicle"),
|
title: t("jobs.fields.vehicle"),
|
||||||
dataIndex: "vehicleid",
|
dataIndex: "vehicleid",
|
||||||
key: "vehicleid",
|
key: "vehicleid",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(
|
||||||
|
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||||
|
a.v_model_desc || ""
|
||||||
|
}`,
|
||||||
|
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||||
|
),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "vehicleid" && state.sortedInfo.order,
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
record.vehicleid ? (
|
record.vehicleid ? (
|
||||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>
|
<Link to={`/manage/vehicles/${record.vehicleid}`}>
|
||||||
@@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({bodyshop, owner}) {
|
|||||||
title: t("jobs.fields.status"),
|
title: t("jobs.fields.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
sorter: (a, b) =>
|
||||||
|
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
|
||||||
|
text: status,
|
||||||
|
value: status,
|
||||||
|
})),
|
||||||
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,8 +100,7 @@ export function PartsQueueListComponent({bodyshop}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
const handleOnRowClick = (record) => {
|
||||||
if (record) {
|
if (record?.id) {
|
||||||
if (record.id) {
|
|
||||||
history({
|
history({
|
||||||
search: queryString.stringify({
|
search: queryString.stringify({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
@@ -109,7 +108,6 @@ export function PartsQueueListComponent({bodyshop}) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -354,7 +352,15 @@ export function PartsQueueListComponent({bodyshop}) {
|
|||||||
},
|
},
|
||||||
selectedRowKeys: [selected],
|
selectedRowKeys: [selected],
|
||||||
type: "radio",
|
type: "radio",
|
||||||
}}/>
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {BranchesOutlined, PauseCircleOutlined} from "@ant-design/icons";
|
import {BranchesOutlined, PauseCircleOutlined} from "@ant-design/icons";
|
||||||
import {Checkbox,Space, Tooltip} from "antd";
|
import {Checkbox,Space, Tooltip} from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {TimeFormatter} from "../../utils/DateFormatter";
|
import {TimeFormatter} from "../../utils/DateFormatter";
|
||||||
@@ -214,17 +213,12 @@ const r = ({technician, state, activeStatuses, data, bodyshop, refetch}) => {
|
|||||||
state.sortedInfo.columnKey === "date_next_contact" &&
|
state.sortedInfo.columnKey === "date_next_contact" &&
|
||||||
state.sortedInfo.order,
|
state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span
|
<ProductionListDate
|
||||||
style={{
|
record={record}
|
||||||
color:
|
field="date_next_contact"
|
||||||
record.date_next_contact &&
|
pastIndicator
|
||||||
dayjs(record.date_next_contact).isBefore(dayjs())
|
time
|
||||||
? "red"
|
/>
|
||||||
: "",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProductionListDate record={record} field="date_next_contact" time/>
|
|
||||||
</span>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {DeleteFilled} from "@ant-design/icons";
|
import {DeleteFilled} from "@ant-design/icons";
|
||||||
import {Button, Form, Input, InputNumber, Select, Switch, Typography,} from "antd";
|
import {Button, Form, Input, InputNumber, Select, Space, Switch, Typography,} from "antd";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@@ -11,6 +11,7 @@ import {createStructuredSelector} from "reselect";
|
|||||||
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
||||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
||||||
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
|
||||||
const SelectorDiv = styled.div`
|
const SelectorDiv = styled.div`
|
||||||
.ant-form-item .ant-select {
|
.ant-form-item .ant-select {
|
||||||
@@ -182,7 +183,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
|
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
|
||||||
<Form.List name={["cdk_configuration", "payers"]}>
|
<Form.List name={["cdk_configuration", "payers"]}>
|
||||||
{(fields, {add, remove}) => {
|
{(fields, {add, remove, move}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -240,11 +241,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<DeleteFilled
|
<Space align="center">
|
||||||
onClick={() => {
|
d
|
||||||
remove(field.name);
|
onClick={() => {
|
||||||
}}
|
remove(field.name);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
@@ -336,7 +344,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
id="costs"
|
id="costs"
|
||||||
>
|
>
|
||||||
<Form.List name={["md_responsibility_centers", "costs"]}>
|
<Form.List name={["md_responsibility_centers", "costs"]}>
|
||||||
{(fields, {add, remove}) => {
|
{(fields, {add, remove, move}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -453,12 +461,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
<Input onBlur={handleBlur}/>
|
<Input onBlur={handleBlur}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
<Space align="center">
|
||||||
<DeleteFilled
|
<DeleteFilled
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
remove(field.name);
|
remove(field.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
@@ -484,7 +498,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
id="profits"
|
id="profits"
|
||||||
>
|
>
|
||||||
<Form.List name={["md_responsibility_centers", "profits"]}>
|
<Form.List name={["md_responsibility_centers", "profits"]}>
|
||||||
{(fields, {add, remove}) => {
|
{(fields, {add, remove, move}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -586,11 +600,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
<Input onBlur={handleBlur}/>
|
<Input onBlur={handleBlur}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
<DeleteFilled
|
<Space align="center">
|
||||||
onClick={() => {
|
<DeleteFilled
|
||||||
remove(field.name);
|
onClick={() => {
|
||||||
}}
|
remove(field.name);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
@@ -615,7 +636,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) {
|
|||||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||||
<>
|
<>
|
||||||
<Form.List name={["md_responsibility_centers", "dms_defaults"]}>
|
<Form.List name={["md_responsibility_centers", "dms_defaults"]}>
|
||||||
{(fields, {add, remove}) => {
|
{(fields, {add, remove, move}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {Link} from "react-router-dom";
|
|||||||
import {createStructuredSelector} from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import {alphaSort, statusSort} from "../../utils/sorters";
|
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, {
|
||||||
|
OwnerNameDisplayFunction,
|
||||||
|
} from "../owner-name-display/owner-name-display.component";
|
||||||
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({vehicle, bodyshop}) {
|
|||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
key: "owner",
|
key: "owner",
|
||||||
|
sorter: (a, b) =>
|
||||||
|
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Link to={`/manage/owners/${record.owner.id}`}>
|
<Link to={`/manage/owners/${record.owner.id}`}>
|
||||||
<OwnerNameDisplay ownerObject={record}/>
|
<OwnerNameDisplay ownerObject={record}/>
|
||||||
@@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({vehicle, bodyshop}) {
|
|||||||
title: t("jobs.fields.status"),
|
title: t("jobs.fields.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
sorter: (a, b) =>
|
||||||
|
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
|
||||||
|
text: status,
|
||||||
|
value: status,
|
||||||
|
})),
|
||||||
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next";
|
|||||||
import {Link, useLocation, useNavigate} from "react-router-dom";
|
import {Link, useLocation, useNavigate} from "react-router-dom";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
import {pageLimit} from "../../utils/config";
|
import {pageLimit} from "../../utils/config";
|
||||||
|
import { alphaSort } from '../../utils/sorters';
|
||||||
|
|
||||||
export default function VehiclesListComponent({
|
export default function VehiclesListComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -32,6 +33,8 @@ export default function VehiclesListComponent({
|
|||||||
title: t("vehicles.fields.v_vin"),
|
title: t("vehicles.fields.v_vin"),
|
||||||
dataIndex: "v_vin",
|
dataIndex: "v_vin",
|
||||||
key: "v_vin",
|
key: "v_vin",
|
||||||
|
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "v_vin" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Link to={"/manage/vehicles/" + record.id}>
|
<Link to={"/manage/vehicles/" + record.id}>
|
||||||
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
|
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
|
||||||
@@ -52,8 +55,10 @@ export default function VehiclesListComponent({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("vehicles.fields.plate_no"),
|
title: t("vehicles.fields.plate_no"),
|
||||||
dataIndex: "plate",
|
dataIndex: "plate_no",
|
||||||
key: "plate",
|
key: "plate_no",
|
||||||
|
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>
|
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {getAuth, updatePassword, updateProfile} from "firebase/auth";
|
|||||||
import {getFirestore} from "firebase/firestore";
|
import {getFirestore} from "firebase/firestore";
|
||||||
import {getMessaging, getToken, onMessage} from "firebase/messaging";
|
import {getMessaging, getToken, onMessage} from "firebase/messaging";
|
||||||
import {store} from "../redux/store";
|
import {store} from "../redux/store";
|
||||||
|
import axios from "axios";
|
||||||
|
import { checkBeta } from "../utils/handleBeta";
|
||||||
|
|
||||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||||
initializeApp(config);
|
initializeApp(config);
|
||||||
@@ -86,6 +88,17 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
|||||||
null,
|
null,
|
||||||
...additionalParams,
|
...additionalParams,
|
||||||
};
|
};
|
||||||
|
axios.post("/ioevent", {
|
||||||
|
useremail:
|
||||||
|
(state.user && state.user.currentUser && state.user.currentUser.email) ||
|
||||||
|
null,
|
||||||
|
bodyshopid:
|
||||||
|
(state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
|
||||||
|
operationName: eventName,
|
||||||
|
variables: additionalParams,
|
||||||
|
dbevent: false,
|
||||||
|
env: checkBeta() ? "beta" : "master",
|
||||||
|
});
|
||||||
// console.log(
|
// console.log(
|
||||||
// "%c[Analytics]",
|
// "%c[Analytics]",
|
||||||
// "background-color: green ;font-weight:bold;",
|
// "background-color: green ;font-weight:bold;",
|
||||||
|
|||||||
@@ -1,47 +1,48 @@
|
|||||||
import {gql} from "@apollo/client";
|
import {gql} from "@apollo/client";
|
||||||
|
|
||||||
export const QUERY_VEHICLE_BY_ID = gql`
|
export const QUERY_VEHICLE_BY_ID = gql`
|
||||||
query QUERY_VEHICLE_BY_ID($id: uuid!) {
|
query QUERY_VEHICLE_BY_ID($id: uuid!) {
|
||||||
vehicles_by_pk(id: $id) {
|
vehicles_by_pk(id: $id) {
|
||||||
created_at
|
created_at
|
||||||
db_v_code
|
db_v_code
|
||||||
id
|
id
|
||||||
plate_no
|
plate_no
|
||||||
plate_st
|
plate_st
|
||||||
v_vin
|
v_vin
|
||||||
v_type
|
v_type
|
||||||
v_trimcode
|
v_trimcode
|
||||||
v_tone
|
v_tone
|
||||||
v_stage
|
v_stage
|
||||||
v_prod_dt
|
v_prod_dt
|
||||||
v_paint_codes
|
v_paint_codes
|
||||||
v_options
|
v_options
|
||||||
v_model_yr
|
v_model_yr
|
||||||
v_model_desc
|
v_model_desc
|
||||||
v_mldgcode
|
v_mldgcode
|
||||||
v_makecode
|
v_makecode
|
||||||
v_make_desc
|
v_make_desc
|
||||||
v_engine
|
v_engine
|
||||||
v_cond
|
v_cond
|
||||||
v_color
|
v_color
|
||||||
v_bstyle
|
v_bstyle
|
||||||
updated_at
|
updated_at
|
||||||
trim_color
|
trim_color
|
||||||
notes
|
notes
|
||||||
jobs(order_by: { date_open: desc }) {
|
jobs(order_by: { date_open: desc }) {
|
||||||
id
|
id
|
||||||
ro_number
|
ro_number
|
||||||
ownr_fn
|
ownr_co_nm
|
||||||
ownr_ln
|
ownr_fn
|
||||||
owner {
|
ownr_ln
|
||||||
id
|
owner {
|
||||||
}
|
id
|
||||||
clm_no
|
|
||||||
status
|
|
||||||
clm_total
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
clm_no
|
||||||
|
status
|
||||||
|
clm_total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const UPDATE_VEHICLE = gql`
|
export const UPDATE_VEHICLE = gql`
|
||||||
|
|||||||
@@ -622,7 +622,7 @@
|
|||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
"controllist": "Control Number List",
|
"controllist": "Control Number List",
|
||||||
"payers": "CDK Payers"
|
"payers": "Payers"
|
||||||
},
|
},
|
||||||
"cdk_dealerid": "CDK Dealer ID",
|
"cdk_dealerid": "CDK Dealer ID",
|
||||||
"pbs_serialnumber": "PBS Serial Number",
|
"pbs_serialnumber": "PBS Serial Number",
|
||||||
@@ -858,13 +858,20 @@
|
|||||||
"creating": "Error creating survey {{message}}",
|
"creating": "Error creating survey {{message}}",
|
||||||
"notconfigured": "You do not have any current CSI Question Sets configured.",
|
"notconfigured": "You do not have any current CSI Question Sets configured.",
|
||||||
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
|
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
|
||||||
"notfoundtitle": "No survey found."
|
"notfoundtitle": "No survey found.",
|
||||||
|
"surveycompletesubtitle": "",
|
||||||
|
"surveycompletetitle": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completedon": "Completed On",
|
"completedon": "Completed On",
|
||||||
"created_at": "Created At"
|
"created_at": "Created At",
|
||||||
|
"surveyid": "",
|
||||||
|
"validuntil": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"copyright": "",
|
||||||
|
"greeting": "",
|
||||||
|
"intro": "",
|
||||||
"nologgedinuser": "Please log out of {{app}}",
|
"nologgedinuser": "Please log out of {{app}}",
|
||||||
"nologgedinuser_sub": "Users of {{app}} cannot complete CSI surveys while logged in. Please log out and try again.",
|
"nologgedinuser_sub": "Users of {{app}} cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||||
"noneselected": "No response selected.",
|
"noneselected": "No response selected.",
|
||||||
@@ -1277,14 +1284,20 @@
|
|||||||
"columns": {
|
"columns": {
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"end": "End",
|
"end": "End",
|
||||||
|
"human_readable": "Human Readable",
|
||||||
|
"percentage": "Percentage",
|
||||||
"relative_end": "Relative End",
|
"relative_end": "Relative End",
|
||||||
"relative_start": "Relative Start",
|
"relative_start": "Relative Start",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"status": "Status",
|
||||||
|
"status_count": "In Status",
|
||||||
"value": "Value"
|
"value": "Value"
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
|
"calculated_based_on": "Calculated based on",
|
||||||
"current_status_accumulated_time": "Current Status Accumulated Time",
|
"current_status_accumulated_time": "Current Status Accumulated Time",
|
||||||
"data_unavailable": " There is currently no Lifecycle data for this Job.",
|
"data_unavailable": " There is currently no Lifecycle data for this Job.",
|
||||||
|
"jobs_in_since": "Jobs in since",
|
||||||
"legend_title": "Legend",
|
"legend_title": "Legend",
|
||||||
"loading": "Loading Job Timelines....",
|
"loading": "Loading Job Timelines....",
|
||||||
"not_available": "N/A",
|
"not_available": "N/A",
|
||||||
@@ -1296,6 +1309,10 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "Error getting Job Lifecycle Data"
|
"fetch": "Error getting Job Lifecycle Data"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"dashboard": "Job Lifecycle",
|
||||||
|
"top_durations": "Top Durations"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"job_payments": {
|
"job_payments": {
|
||||||
@@ -1915,6 +1932,7 @@
|
|||||||
"job": "Job Details",
|
"job": "Job Details",
|
||||||
"jobcosting": "Job Costing",
|
"jobcosting": "Job Costing",
|
||||||
"jobtotals": "Job Totals",
|
"jobtotals": "Job Totals",
|
||||||
|
"labor_hrs": "B/P/T Hrs",
|
||||||
"labor_rates_subtotal": "Labor Rates Subtotal",
|
"labor_rates_subtotal": "Labor Rates Subtotal",
|
||||||
"laborallocations": "Labor Allocations",
|
"laborallocations": "Labor Allocations",
|
||||||
"labortotals": "Labor Totals",
|
"labortotals": "Labor Totals",
|
||||||
@@ -2539,6 +2557,7 @@
|
|||||||
"invoice_total_payable": "Invoice (Total Payable)",
|
"invoice_total_payable": "Invoice (Total Payable)",
|
||||||
"iou_form": "IOU Form",
|
"iou_form": "IOU Form",
|
||||||
"job_costing_ro": "Job Costing",
|
"job_costing_ro": "Job Costing",
|
||||||
|
"job_lifecycle_ro": "",
|
||||||
"job_notes": "Job Notes",
|
"job_notes": "Job Notes",
|
||||||
"key_tag": "Key Tag",
|
"key_tag": "Key Tag",
|
||||||
"labels": {
|
"labels": {
|
||||||
|
|||||||
@@ -858,13 +858,20 @@
|
|||||||
"creating": "",
|
"creating": "",
|
||||||
"notconfigured": "",
|
"notconfigured": "",
|
||||||
"notfoundsubtitle": "",
|
"notfoundsubtitle": "",
|
||||||
"notfoundtitle": ""
|
"notfoundtitle": "",
|
||||||
|
"surveycompletesubtitle": "",
|
||||||
|
"surveycompletetitle": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completedon": "",
|
"completedon": "",
|
||||||
"created_at": ""
|
"created_at": "",
|
||||||
|
"surveyid": "",
|
||||||
|
"validuntil": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"copyright": "",
|
||||||
|
"greeting": "",
|
||||||
|
"intro": "",
|
||||||
"nologgedinuser": "",
|
"nologgedinuser": "",
|
||||||
"nologgedinuser_sub": "",
|
"nologgedinuser_sub": "",
|
||||||
"noneselected": "",
|
"noneselected": "",
|
||||||
@@ -1277,14 +1284,20 @@
|
|||||||
"columns": {
|
"columns": {
|
||||||
"duration": "",
|
"duration": "",
|
||||||
"end": "",
|
"end": "",
|
||||||
|
"human_readable": "",
|
||||||
|
"percentage": "",
|
||||||
"relative_end": "",
|
"relative_end": "",
|
||||||
"relative_start": "",
|
"relative_start": "",
|
||||||
"start": "",
|
"start": "",
|
||||||
|
"status": "",
|
||||||
|
"status_count": "",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
|
"calculated_based_on": "",
|
||||||
"current_status_accumulated_time": "",
|
"current_status_accumulated_time": "",
|
||||||
"data_unavailable": "",
|
"data_unavailable": "",
|
||||||
|
"jobs_in_since": "",
|
||||||
"legend_title": "",
|
"legend_title": "",
|
||||||
"loading": "",
|
"loading": "",
|
||||||
"not_available": "",
|
"not_available": "",
|
||||||
@@ -1296,6 +1309,10 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
|
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"dashboard": "",
|
||||||
|
"top_durations": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"job_payments": {
|
"job_payments": {
|
||||||
@@ -1915,6 +1932,7 @@
|
|||||||
"job": "",
|
"job": "",
|
||||||
"jobcosting": "",
|
"jobcosting": "",
|
||||||
"jobtotals": "",
|
"jobtotals": "",
|
||||||
|
"labor_hrs": "",
|
||||||
"labor_rates_subtotal": "",
|
"labor_rates_subtotal": "",
|
||||||
"laborallocations": "",
|
"laborallocations": "",
|
||||||
"labortotals": "",
|
"labortotals": "",
|
||||||
@@ -2539,6 +2557,7 @@
|
|||||||
"invoice_total_payable": "",
|
"invoice_total_payable": "",
|
||||||
"iou_form": "",
|
"iou_form": "",
|
||||||
"job_costing_ro": "",
|
"job_costing_ro": "",
|
||||||
|
"job_lifecycle_ro": "",
|
||||||
"job_notes": "",
|
"job_notes": "",
|
||||||
"key_tag": "",
|
"key_tag": "",
|
||||||
"labels": {
|
"labels": {
|
||||||
|
|||||||
@@ -858,13 +858,20 @@
|
|||||||
"creating": "",
|
"creating": "",
|
||||||
"notconfigured": "",
|
"notconfigured": "",
|
||||||
"notfoundsubtitle": "",
|
"notfoundsubtitle": "",
|
||||||
"notfoundtitle": ""
|
"notfoundtitle": "",
|
||||||
|
"surveycompletesubtitle": "",
|
||||||
|
"surveycompletetitle": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completedon": "",
|
"completedon": "",
|
||||||
"created_at": ""
|
"created_at": "",
|
||||||
|
"surveyid": "",
|
||||||
|
"validuntil": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"copyright": "",
|
||||||
|
"greeting": "",
|
||||||
|
"intro": "",
|
||||||
"nologgedinuser": "",
|
"nologgedinuser": "",
|
||||||
"nologgedinuser_sub": "",
|
"nologgedinuser_sub": "",
|
||||||
"noneselected": "",
|
"noneselected": "",
|
||||||
@@ -1277,14 +1284,20 @@
|
|||||||
"columns": {
|
"columns": {
|
||||||
"duration": "",
|
"duration": "",
|
||||||
"end": "",
|
"end": "",
|
||||||
|
"human_readable": "",
|
||||||
|
"percentage": "",
|
||||||
"relative_end": "",
|
"relative_end": "",
|
||||||
"relative_start": "",
|
"relative_start": "",
|
||||||
"start": "",
|
"start": "",
|
||||||
|
"status": "",
|
||||||
|
"status_count": "",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
"content": {
|
"content": {
|
||||||
|
"calculated_based_on": "",
|
||||||
"current_status_accumulated_time": "",
|
"current_status_accumulated_time": "",
|
||||||
"data_unavailable": "",
|
"data_unavailable": "",
|
||||||
|
"jobs_in_since": "",
|
||||||
"legend_title": "",
|
"legend_title": "",
|
||||||
"loading": "",
|
"loading": "",
|
||||||
"not_available": "",
|
"not_available": "",
|
||||||
@@ -1296,6 +1309,10 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
|
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"dashboard": "",
|
||||||
|
"top_durations": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"job_payments": {
|
"job_payments": {
|
||||||
@@ -1915,6 +1932,7 @@
|
|||||||
"job": "",
|
"job": "",
|
||||||
"jobcosting": "",
|
"jobcosting": "",
|
||||||
"jobtotals": "",
|
"jobtotals": "",
|
||||||
|
"labor_hrs": "",
|
||||||
"labor_rates_subtotal": "",
|
"labor_rates_subtotal": "",
|
||||||
"laborallocations": "",
|
"laborallocations": "",
|
||||||
"labortotals": "",
|
"labortotals": "",
|
||||||
@@ -2539,6 +2557,7 @@
|
|||||||
"invoice_total_payable": "",
|
"invoice_total_payable": "",
|
||||||
"iou_form": "",
|
"iou_form": "",
|
||||||
"job_costing_ro": "",
|
"job_costing_ro": "",
|
||||||
|
"job_lifecycle_ro": "",
|
||||||
"job_notes": "",
|
"job_notes": "",
|
||||||
"key_tag": "",
|
"key_tag": "",
|
||||||
"labels": {
|
"labels": {
|
||||||
|
|||||||
@@ -600,6 +600,14 @@ export const TemplateList = (type, context) => {
|
|||||||
group: "financial",
|
group: "financial",
|
||||||
enhanced_payroll: true,
|
enhanced_payroll: true,
|
||||||
},
|
},
|
||||||
|
job_lifecycle_ro: {
|
||||||
|
title: i18n.t("printcenter.jobs.job_lifecycle_ro"),
|
||||||
|
description: "",
|
||||||
|
subject: i18n.t("printcenter.jobs.job_lifecycle_ro"),
|
||||||
|
key: "job_lifecycle_ro",
|
||||||
|
disabled: false,
|
||||||
|
group: "post",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(!type || type === "job_special"
|
...(!type || type === "job_special"
|
||||||
@@ -2221,6 +2229,30 @@ export const TemplateList = (type, context) => {
|
|||||||
datedisable: true,
|
datedisable: true,
|
||||||
group: "customers",
|
group: "customers",
|
||||||
},
|
},
|
||||||
|
job_lifecycle_date_detail: {
|
||||||
|
title: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
|
||||||
|
subject: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
|
||||||
|
key: "job_lifecycle_date_detail",
|
||||||
|
//idtype: "vendor",
|
||||||
|
disabled: false,
|
||||||
|
rangeFilter: {
|
||||||
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
|
field: i18n.t("jobs.fields.date_invoiced"),
|
||||||
|
},
|
||||||
|
group: "jobs",
|
||||||
|
},
|
||||||
|
job_lifecycle_date_summary: {
|
||||||
|
title: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
|
||||||
|
subject: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
|
||||||
|
key: "job_lifecycle_date_summary",
|
||||||
|
//idtype: "vendor",
|
||||||
|
disabled: false,
|
||||||
|
rangeFilter: {
|
||||||
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
|
field: i18n.t("jobs.fields.date_invoiced"),
|
||||||
|
},
|
||||||
|
group: "jobs",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(!type || type === "courtesycarcontract"
|
...(!type || type === "courtesycarcontract"
|
||||||
|
|||||||
@@ -4200,7 +4200,7 @@
|
|||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
timeout_sec: 60
|
timeout_sec: 60
|
||||||
webhook_from_env: HASURA_API_URL
|
webhook: https://worktest.home.irony.online
|
||||||
headers:
|
headers:
|
||||||
- name: event-secret
|
- name: event-secret
|
||||||
value_from_env: EVENT_SECRET
|
value_from_env: EVENT_SECRET
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."ioevents" add column "useremail" text
|
||||||
|
-- not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."ioevents" add column "useremail" text;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."ioevents" add column "bodyshopid" uuid
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."ioevents" add column "bodyshopid" uuid
|
||||||
|
null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."ioevents" alter column "useremail" set not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."ioevents" alter column "useremail" drop not null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."ioevents" add column "env" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."ioevents" add column "env" text
|
||||||
|
null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS "public"."ioevents_useremail";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX "ioevents_useremail" on
|
||||||
|
"public"."ioevents" using btree ("useremail");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
alter table "public"."ioevents"
|
||||||
|
add constraint "ioevents_useremail_fkey"
|
||||||
|
foreign key ("useremail")
|
||||||
|
references "public"."users"
|
||||||
|
("email") on update set null on delete set null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
alter table "public"."ioevents"
|
||||||
|
add constraint "ioevents_bodyshopid_fkey"
|
||||||
|
foreign key ("bodyshopid")
|
||||||
|
references "public"."bodyshops"
|
||||||
|
("id") on update set null on delete set null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX IF EXISTS "public"."idx_audit_trail_type";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX "idx_audit_trail_type" on
|
||||||
|
"public"."audit_trail" using btree ("type");
|
||||||
@@ -193,28 +193,33 @@ exports.default = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
|
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
|
||||||
} catch (error) {
|
|
||||||
logger.log("kaizen-sftp-error", "ERROR", "api", null, {
|
|
||||||
...error,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
sftp.end();
|
|
||||||
}
|
|
||||||
sendServerEmail({
|
|
||||||
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
|
||||||
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
|
||||||
Uploaded: ${JSON.stringify(
|
|
||||||
allxmlsToUpload.map((x) => ({filename: x.filename, count: x.count})),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
res.sendStatus(200);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(200).json(error);
|
logger.log("kaizen-sftp-error", "ERROR", "api", null, {
|
||||||
|
...error,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
sftp.end();
|
||||||
}
|
}
|
||||||
|
// sendServerEmail({
|
||||||
|
// subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
||||||
|
// text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
||||||
|
// Uploaded: ${JSON.stringify(
|
||||||
|
// allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
|
||||||
|
// null,
|
||||||
|
// 2
|
||||||
|
// )}
|
||||||
|
// `,
|
||||||
|
// });
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(200).json(error);
|
||||||
|
sendServerEmail({
|
||||||
|
subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`,
|
||||||
|
text: `Errors: JSON.stringify(error)}
|
||||||
|
All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||||
|
|||||||
@@ -11,27 +11,40 @@ require("dotenv").config({
|
|||||||
});
|
});
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
const {operationName, time, dbevent, user, imexshopid} = req.body;
|
const {
|
||||||
|
useremail,
|
||||||
|
bodyshopid,
|
||||||
|
operationName,
|
||||||
|
variables,
|
||||||
|
env,
|
||||||
|
time,
|
||||||
|
dbevent,
|
||||||
|
user,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// await client.request(queries.INSERT_IOEVENT, {
|
await client.request(queries.INSERT_IOEVENT, {
|
||||||
// event: {
|
event: {
|
||||||
// operationname: operationName,
|
operationname: operationName,
|
||||||
// time,
|
time,
|
||||||
// dbevent,
|
dbevent,
|
||||||
// },
|
env,
|
||||||
// });
|
variables,
|
||||||
console.log("IOEVENT", operationName, time, dbevent, user, imexshopid);
|
bodyshopid,
|
||||||
logger.log("ioevent", "trace", user, null, {
|
useremail,
|
||||||
imexshopid,
|
},
|
||||||
operationName,
|
|
||||||
time,
|
|
||||||
dbevent,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", error);
|
logger.log("ioevent-error", "trace", user, null, {
|
||||||
res.status(400).send(error);
|
operationname: operationName,
|
||||||
|
time,
|
||||||
|
dbevent,
|
||||||
|
env,
|
||||||
|
variables,
|
||||||
|
bodyshopid,
|
||||||
|
useremail,
|
||||||
|
});
|
||||||
|
res.sendStatus(200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const queries = require("../graphql-client/queries");
|
|||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
const durationToHumanReadable = require("../utils/durationToHumanReadable");
|
const durationToHumanReadable = require("../utils/durationToHumanReadable");
|
||||||
const calculateStatusDuration = require("../utils/calculateStatusDuration");
|
const calculateStatusDuration = require("../utils/calculateStatusDuration");
|
||||||
|
const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
|
||||||
|
|
||||||
const jobLifecycle = async (req, res) => {
|
const jobLifecycle = async (req, res) => {
|
||||||
// Grab the jobids and statuses from the request body
|
// Grab the jobids and statuses from the request body
|
||||||
@@ -28,12 +29,12 @@ const jobLifecycle = async (req, res) => {
|
|||||||
jobIDs,
|
jobIDs,
|
||||||
transitions: []
|
transitions: []
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
|
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
|
||||||
|
|
||||||
const groupedTransitions = {};
|
const groupedTransitions = {};
|
||||||
|
const allDurations = [];
|
||||||
|
|
||||||
for (let jobId in transitionsByJobId) {
|
for (let jobId in transitionsByJobId) {
|
||||||
let lifecycle = transitionsByJobId[jobId].map(transition => {
|
let lifecycle = transitionsByJobId[jobId].map(transition => {
|
||||||
@@ -53,15 +54,57 @@ const jobLifecycle = async (req, res) => {
|
|||||||
return transition;
|
return transition;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||||
|
|
||||||
groupedTransitions[jobId] = {
|
groupedTransitions[jobId] = {
|
||||||
lifecycle: lifecycle,
|
lifecycle,
|
||||||
durations: calculateStatusDuration(lifecycle, statuses),
|
durations
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (durations?.summations) {
|
||||||
|
allDurations.push(durations.summations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalSummations = [];
|
||||||
|
const flatGroupedAllDurations = _.groupBy(allDurations.flat(),'status');
|
||||||
|
|
||||||
|
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
|
||||||
|
acc[status] = flatGroupedAllDurations[status].length;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
// Calculate total value of all statuses
|
||||||
|
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
|
||||||
|
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
Object.keys(flatGroupedAllDurations).forEach(status => {
|
||||||
|
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
|
||||||
|
const humanReadable = durationToHumanReadable(moment.duration(value));
|
||||||
|
const percentage = (value / finalTotal) * 100;
|
||||||
|
const color = getLifecycleStatusColor(status);
|
||||||
|
const roundedPercentage = `${Math.round(percentage)}%`;
|
||||||
|
finalSummations.push({
|
||||||
|
status,
|
||||||
|
value,
|
||||||
|
humanReadable,
|
||||||
|
percentage,
|
||||||
|
color,
|
||||||
|
roundedPercentage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
jobIDs,
|
jobIDs,
|
||||||
transition: groupedTransitions,
|
transition: groupedTransitions,
|
||||||
|
durations: {
|
||||||
|
jobs: jobIDs.length,
|
||||||
|
summations: finalSummations,
|
||||||
|
totalStatuses: finalSummations.length,
|
||||||
|
total: finalTotal,
|
||||||
|
statusCounts: finalStatusCounts,
|
||||||
|
humanReadable: durationToHumanReadable(moment.duration(finalTotal))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
const durationToHumanReadable = require("./durationToHumanReadable");
|
const durationToHumanReadable = require("./durationToHumanReadable");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
|
const getLifecycleStatusColor = require("./getLifecycleStatusColor");
|
||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const getColor = (key) => {
|
|
||||||
const hash = crypto.createHash('sha256');
|
|
||||||
hash.update(key);
|
|
||||||
const hashedKey = hash.digest('hex');
|
|
||||||
const num = parseInt(hashedKey, 16);
|
|
||||||
return '#' + (num % 16777215).toString(16).padStart(6, '0');
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateStatusDuration = (transitions, statuses) => {
|
const calculateStatusDuration = (transitions, statuses) => {
|
||||||
let statusDuration = {};
|
let statusDuration = {};
|
||||||
@@ -33,26 +25,16 @@ const calculateStatusDuration = (transitions, statuses) => {
|
|||||||
if (!transition.prev_value) {
|
if (!transition.prev_value) {
|
||||||
statusDuration[transition.value] = {
|
statusDuration[transition.value] = {
|
||||||
value: duration,
|
value: duration,
|
||||||
humanReadable: transition.duration_readable
|
humanReadable: durationToHumanReadable(moment.duration(duration))
|
||||||
};
|
};
|
||||||
} else if (!transition.next_value) {
|
} else {
|
||||||
if (statusDuration[transition.value]) {
|
if (statusDuration[transition.value]) {
|
||||||
statusDuration[transition.value].value += duration;
|
statusDuration[transition.value].value += duration;
|
||||||
statusDuration[transition.value].humanReadable = transition.duration_readable;
|
statusDuration[transition.value].humanReadable = durationToHumanReadable(moment.duration(statusDuration[transition.value].value));
|
||||||
} else {
|
} else {
|
||||||
statusDuration[transition.value] = {
|
statusDuration[transition.value] = {
|
||||||
value: duration,
|
value: duration,
|
||||||
humanReadable: transition.duration_readable
|
humanReadable: durationToHumanReadable(moment.duration(duration))
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (statusDuration[transition.value]) {
|
|
||||||
statusDuration[transition.value].value += duration;
|
|
||||||
statusDuration[transition.value].humanReadable = transition.duration_readable;
|
|
||||||
} else {
|
|
||||||
statusDuration[transition.value] = {
|
|
||||||
value: duration,
|
|
||||||
humanReadable: transition.duration_readable
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +61,7 @@ const calculateStatusDuration = (transitions, statuses) => {
|
|||||||
value,
|
value,
|
||||||
humanReadable,
|
humanReadable,
|
||||||
percentage: statusDuration[status].percentage,
|
percentage: statusDuration[status].percentage,
|
||||||
color: getColor(status),
|
color: getLifecycleStatusColor(status),
|
||||||
roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`
|
roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
11
server/utils/getLifecycleStatusColor.js
Normal file
11
server/utils/getLifecycleStatusColor.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const getLifecycleStatusColor = (key) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
hash.update(key);
|
||||||
|
const hashedKey = hash.digest('hex');
|
||||||
|
const num = parseInt(hashedKey, 16);
|
||||||
|
return '#' + (num % 16777215).toString(16).padStart(6, '0');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getLifecycleStatusColor;
|
||||||
Reference in New Issue
Block a user