Merge branch 'master-beta' into master-AIO

This commit is contained in:
Patrick Fic
2024-03-15 13:35:45 -07:00
39 changed files with 964 additions and 160 deletions

View File

@@ -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>

View File

@@ -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
} `;

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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>

View File

@@ -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),
}, },
{ {

View File

@@ -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>
); );
} }

View File

@@ -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>
), ),
}, },
{ {

View File

@@ -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) => (

View File

@@ -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),
}, },
{ {

View File

@@ -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>

View File

@@ -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;",

View File

@@ -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`

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" add column "useremail" text;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."ioevents" add column "bodyshopid" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" alter column "useremail" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" alter column "useremail" drop not null;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."ioevents" add column "env" text
null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."ioevents_useremail";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "ioevents_useremail" on
"public"."ioevents" using btree ("useremail");

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey";

View File

@@ -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;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey";

View File

@@ -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;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."idx_audit_trail_type";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "idx_audit_trail_type" on
"public"."audit_trail" using btree ("type");

View File

@@ -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) => {

View File

@@ -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);
} }
}; };

View File

@@ -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))
}
}); });
} }

View File

@@ -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)}%`
}); });
} }

View 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;