diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index f07d49c89..17ebd2af4 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1432,6 +1432,27 @@ messages + + admin_job_remove_from_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + admin_jobmarkexported false @@ -1516,6 +1537,90 @@ + + alerttoggle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + appointmentcancel + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + appointmentinsert + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + billdeleted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + billposted false @@ -1663,6 +1768,27 @@ + + jobexported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobfieldchanged false @@ -1726,6 +1852,27 @@ + + jobinvoiced + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobioucreated false @@ -1915,6 +2062,48 @@ + + jobsuspend + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobvoid + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -3204,6 +3393,27 @@ + + federal_tax_exempt + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + generatepartslabel false @@ -3372,6 +3582,27 @@ + + printlabels + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + retailtotal false @@ -5401,6 +5632,32 @@ + + md_functionality_toggles + + + parts_queue_toggle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + md_hour_split @@ -7082,6 +7339,27 @@ + + reportcenter + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + templates false @@ -12219,6 +12497,27 @@ + + insuranceexpired + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + noteconvertedfrom false @@ -12837,6 +13136,27 @@ + + readiness + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + registrationexpires false @@ -13311,6 +13631,53 @@ + + readiness + + + notready + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + ready + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + status @@ -13565,6 +13932,48 @@ + + surveycompletesubtitle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + surveycompletetitle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -13612,11 +14021,116 @@ + + surveyid + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + validuntil + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + labels + + copyright + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + greeting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + intro + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + nologgedinuser false @@ -13894,6 +14408,27 @@ + + phone + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + prodhrs false @@ -13941,6 +14476,48 @@ titles + + labhours + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + larhours + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + monthlyemployeeefficiency false @@ -14130,6 +14707,27 @@ + + scheduledindate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + scheduledintoday false @@ -14151,6 +14749,27 @@ + + scheduledoutdate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + scheduledouttoday false @@ -15836,6 +16455,27 @@ + + active + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + endmustbeafterstart false @@ -15878,6 +16518,27 @@ + + inactive + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + name false @@ -15920,6 +16581,27 @@ + + status + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + straight_time false @@ -18180,6 +18862,27 @@ + + tvmode + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + unknown false @@ -19241,6 +19944,383 @@ + + job_lifecycle + + + columns + + + duration + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + end + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + relative_end + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + relative_start + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + start + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + value + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + content + + + current_status_accumulated_time + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + data_unavailable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + legend_title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + loading + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + not_available + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + previous_status_accumulated_time + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + title_durations + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + title_loading + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + title_transitions + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + errors + + + fetch + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + job_payments @@ -23731,6 +24811,27 @@ + + date_lost_sale + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + date_next_contact false @@ -28317,6 +29418,27 @@ + + calc_scheuled_completion + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + cards @@ -28467,6 +29589,27 @@ + + more + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + notes false @@ -30185,6 +31328,27 @@ + + parts_lines + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + parts_received false @@ -30888,6 +32052,27 @@ + + remove_from_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + returntotals false @@ -31371,6 +32556,27 @@ + + update_scheduled_completion + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + vehicle_info false @@ -33944,6 +35150,27 @@ + + lifecycle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + partssublet false @@ -34690,6 +35917,32 @@ + + render + + + conversation_list + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + @@ -39607,6 +40860,27 @@ + + job_lifecycle_ro + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + job_notes false @@ -42258,6 +43532,258 @@ labels + + advanced_filters + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_false + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_filter_field + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_filter_operator + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_filter_value + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_filters + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_hide + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_show + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_sorter_direction + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_sorter_field + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_sorters + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + advanced_filters_true + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dates false @@ -42714,6 +44240,27 @@ + + ar_aging + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + attendance_detail false @@ -43932,6 +45479,27 @@ + + lost_sales + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + open_orders false @@ -43995,6 +45563,27 @@ + + open_orders_excel + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + open_orders_ins_co false @@ -45332,6 +46921,27 @@ labels + + allemployeetimetickets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + asoftodaytarget false @@ -45353,6 +46963,69 @@ + + body + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + bodyabbrev + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + bodycharttitle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + calendarperiod false @@ -45374,6 +47047,27 @@ + + combinedcharttitle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dailyactual false @@ -45479,6 +47173,27 @@ + + jobscompletednotinvoiced + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + lastmonth false @@ -45542,6 +47257,27 @@ + + priorweek + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + productivestatistics false @@ -45584,6 +47320,69 @@ + + refinish + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + refinishabbrev + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + refinishcharttitle + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + targets false @@ -45668,6 +47467,27 @@ + + timeticketsemployee + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + todateactual false @@ -45689,6 +47509,48 @@ + + total + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + totalhrs + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + totaloverperiod false @@ -49047,6 +50909,90 @@ + + techconsole + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + techjobclock + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + techjoblookup + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + techshiftclock + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + temporarydocs false diff --git a/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx new file mode 100644 index 000000000..417907eb9 --- /dev/null +++ b/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx @@ -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 moment from "moment"; +import DashboardRefreshRequired from "../refresh-required.component"; +import axios from "axios"; + +const fortyFiveDaysAgo = () => moment().subtract(45, 'days').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 {record.status} + } + }, + { + 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 ; + + const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}` + + return ( + + +
+
+ {lifecycleData.summations.map((key, index, array) => { + const isFirst = index === 0; + const isLast = index === array.length - 1; + return ( +
+ + {key.percentage > 15 ? + <> +
{key.roundedPercentage}
+
+ {key.status} +
+ + : null} +
+ ); + })} +
+ +
+ {lifecycleData.summations.map((key) => ( + +
+ {key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage}) +
+
+ ))} +
+
+ + b.value - a.value).slice(0, 3)}/> + + + + + ); +} + +export const JobLifecycleDashboardGQL = ` +job_lifecycle: jobs(where: { + actual_in: { + _gte: "${moment().subtract(45, 'days').toISOString()}" + } +}) { + id + actual_in +} `; \ No newline at end of file diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 7012d943b..2843593b4 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -1,380 +1,391 @@ -import Icon, { SyncOutlined } from "@ant-design/icons"; -import { gql, useMutation, useQuery } from "@apollo/client"; -import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd"; +import Icon, {SyncOutlined} from "@ant-design/icons"; +import {gql, useMutation, useQuery} from "@apollo/client"; +import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd"; import i18next from "i18next"; import _ from "lodash"; import moment from "moment"; -import React, { useState } from "react"; -import { Responsive, WidthProvider } from "react-grid-layout"; -import { useTranslation } from "react-i18next"; -import { MdClose } from "react-icons/md"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { logImEXEvent } from "../../firebase/firebase.utils"; -import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; -import { - selectBodyshop, - selectCurrentUser, -} from "../../redux/user/user.selectors"; +import React, {useState} from "react"; +import {Responsive, WidthProvider} from "react-grid-layout"; +import {useTranslation} from "react-i18next"; +import {MdClose} from "react-icons/md"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {logImEXEvent} from "../../firebase/firebase.utils"; +import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries"; +import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import AlertComponent from "../alert/alert.component"; import DashboardMonthlyEmployeeEfficiency, { - DashboardMonthlyEmployeeEfficiencyGql, + DashboardMonthlyEmployeeEfficiencyGql, } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component"; import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component"; import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component"; import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component"; import DashboardMonthlyRevenueGraph, { - DashboardMonthlyRevenueGraphGql, + DashboardMonthlyRevenueGraphGql, } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; import DashboardProjectedMonthlySales, { - DashboardProjectedMonthlySalesGql, + DashboardProjectedMonthlySalesGql, } from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; -import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component"; +import DashboardTotalProductionDollars + from "../dashboard-components/total-production-dollars/total-production-dollars.component"; import DashboardTotalProductionHours, { - DashboardTotalProductionHoursGql, + DashboardTotalProductionHoursGql, } from "../dashboard-components/total-production-hours/total-production-hours.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; //Combination of the following: // /node_modules/react-grid-layout/css/styles.css // /node_modules/react-resizable/css/styles.css import DashboardScheduledInToday, { - DashboardScheduledInTodayGql, + DashboardScheduledInTodayGql, } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component"; import DashboardScheduledOutToday, { - DashboardScheduledOutTodayGql, + DashboardScheduledOutTodayGql, } 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 { GenerateDashboardData } from "./dashboard-grid.utils"; +import {GenerateDashboardData} from "./dashboard-grid.utils"; const ResponsiveReactGridLayout = WidthProvider(Responsive); const mapStateToProps = createStructuredSelector({ - currentUser: selectCurrentUser, - bodyshop: selectBodyshop, + currentUser: selectCurrentUser, + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function DashboardGridComponent({ currentUser, bodyshop }) { - const { t } = useTranslation(); - const [state, setState] = useState({ - ...(bodyshop.associations[0].user.dashboardlayout - ? bodyshop.associations[0].user.dashboardlayout - : { items: [], layout: {}, layouts: [] }), - }); - - const { loading, error, data, refetch } = useQuery( - createDashboardQuery(state), - { fetchPolicy: "network-only", nextFetchPolicy: "network-only" } - ); - - const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); - - const handleLayoutChange = async (layout, layouts) => { - logImEXEvent("dashboard_change_layout"); - - setState({ ...state, layout, layouts }); - - const result = await updateLayout({ - variables: { - email: currentUser.email, - layout: { ...state, layout, layouts }, - }, +export function DashboardGridComponent({currentUser, bodyshop}) { + const {t} = useTranslation(); + const [state, setState] = useState({ + ...(bodyshop.associations[0].user.dashboardlayout + ? bodyshop.associations[0].user.dashboardlayout + : {items: [], layout: {}, layouts: []}), }); - if (!!result.errors) { - notification["error"]({ - message: t("dashboard.errors.updatinglayout", { - message: JSON.stringify(result.errors), - }), - }); - } - }; - const handleRemoveComponent = (key) => { - logImEXEvent("dashboard_remove_component", { name: key }); - const idxToRemove = state.items.findIndex((i) => i.i === key); - const items = _.cloneDeep(state.items); + const {loading, error, data, refetch} = useQuery( + createDashboardQuery(state), + {fetchPolicy: "network-only", nextFetchPolicy: "network-only"} + ); - items.splice(idxToRemove, 1); - setState({ ...state, items }); - }; + const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); - const handleAddComponent = (e) => { - logImEXEvent("dashboard_add_component", { name: e }); - setState({ - ...state, - items: [ - ...state.items, - { - i: e.key, - x: (state.items.length * 2) % (state.cols || 12), - y: 99, // puts it at the bottom - w: componentList[e.key].w || 2, - h: componentList[e.key].h || 2, - }, - ], - }); - }; + const handleLayoutChange = async (layout, layouts) => { + logImEXEvent("dashboard_change_layout"); - const dashboarddata = React.useMemo( - () => GenerateDashboardData(data), - [data] - ); - const existingLayoutKeys = state.items.map((i) => i.i); - const addComponentOverlay = ( - - {Object.keys(componentList).map((key) => ( - - {componentList[key].label} - - ))} - - ); + setState({...state, layout, layouts}); - if (error) return ; - - return ( -
- - - - - - + const result = await updateLayout({ + variables: { + email: currentUser.email, + layout: {...state, layout, layouts}, + }, + }); + if (!!result.errors) { + notification["error"]({ + message: t("dashboard.errors.updatinglayout", { + message: JSON.stringify(result.errors), + }), + }); } - /> + }; + const handleRemoveComponent = (key) => { + logImEXEvent("dashboard_remove_component", {name: key}); + const idxToRemove = state.items.findIndex((i) => i.i === key); - - {state.items.map((item, index) => { - const TheComponent = componentList[item.i].component; - return ( -
{ + logImEXEvent("dashboard_add_component", {name: e}); + setState({ + ...state, + items: [ + ...state.items, + { + i: e.key, + x: (state.items.length * 2) % (state.cols || 12), + y: 99, // puts it at the bottom + w: componentList[e.key].w || 2, + h: componentList[e.key].h || 2, + }, + ], + }); + }; + + const dashboarddata = React.useMemo( + () => GenerateDashboardData(data), + [data] + ); + const existingLayoutKeys = state.items.map((i) => i.i); + const addComponentOverlay = ( + + {Object.keys(componentList).map((key) => ( + + {componentList[key].label} + + ))} + + ); + + if (error) return ; + + return ( +
+ + + + + + + } + /> + + - - handleRemoveComponent(item.i)} - /> - - -
- ); - })} - -
- ); + {state.items.map((item, index) => { + const TheComponent = componentList[item.i].component; + return ( +
+ + handleRemoveComponent(item.i)} + /> + + +
+ ); + })} +
+
+ ); } export default connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps, + mapDispatchToProps )(DashboardGridComponent); const componentList = { - ProductionDollars: { - label: i18next.t("dashboard.titles.productiondollars"), - component: DashboardTotalProductionDollars, - gqlFragment: null, - w: 1, - h: 1, - minW: 2, - minH: 1, - }, - ProductionHours: { - label: i18next.t("dashboard.titles.productionhours"), - component: DashboardTotalProductionHours, - gqlFragment: DashboardTotalProductionHoursGql, - w: 3, - h: 1, - minW: 3, - minH: 1, - }, - ProjectedMonthlySales: { - label: i18next.t("dashboard.titles.projectedmonthlysales"), - component: DashboardProjectedMonthlySales, - gqlFragment: DashboardProjectedMonthlySalesGql, - w: 2, - h: 1, - minW: 2, - minH: 1, - }, - MonthlyRevenueGraph: { - label: i18next.t("dashboard.titles.monthlyrevenuegraph"), - component: DashboardMonthlyRevenueGraph, - gqlFragment: DashboardMonthlyRevenueGraphGql, - w: 4, - h: 2, - minW: 4, - minH: 2, - }, - MonthlyJobCosting: { - label: i18next.t("dashboard.titles.monthlyjobcosting"), - component: DashboardMonthlyJobCosting, - gqlFragment: null, - minW: 6, - minH: 3, - w: 6, - h: 3, - }, - MonthlyPartsSales: { - label: i18next.t("dashboard.titles.monthlypartssales"), - component: DashboardMonthlyPartsSales, - gqlFragment: null, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - MonthlyLaborSales: { - label: i18next.t("dashboard.titles.monthlylaborsales"), - component: DashboardMonthlyLaborSales, - gqlFragment: null, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - MonthlyEmployeeEfficency: { - label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), - component: DashboardMonthlyEmployeeEfficiency, - gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, - minW: 2, - minH: 2, - w: 2, - h: 2, - }, - ScheduleInToday: { - label: i18next.t("dashboard.titles.scheduledintoday"), - component: DashboardScheduledInToday, - gqlFragment: DashboardScheduledInTodayGql, - minW: 6, - minH: 2, - w: 10, - h: 3, - }, - ScheduleOutToday: { - label: i18next.t("dashboard.titles.scheduledouttoday"), - component: DashboardScheduledOutToday, - gqlFragment: DashboardScheduledOutTodayGql, - minW: 6, - minH: 2, - w: 10, - h: 3, - }, + ProductionDollars: { + label: i18next.t("dashboard.titles.productiondollars"), + component: DashboardTotalProductionDollars, + gqlFragment: null, + w: 1, + h: 1, + minW: 2, + minH: 1, + }, + ProductionHours: { + label: i18next.t("dashboard.titles.productionhours"), + component: DashboardTotalProductionHours, + gqlFragment: DashboardTotalProductionHoursGql, + w: 3, + h: 1, + minW: 3, + minH: 1, + }, + ProjectedMonthlySales: { + label: i18next.t("dashboard.titles.projectedmonthlysales"), + component: DashboardProjectedMonthlySales, + gqlFragment: DashboardProjectedMonthlySalesGql, + w: 2, + h: 1, + minW: 2, + minH: 1, + }, + MonthlyRevenueGraph: { + label: i18next.t("dashboard.titles.monthlyrevenuegraph"), + component: DashboardMonthlyRevenueGraph, + gqlFragment: DashboardMonthlyRevenueGraphGql, + w: 4, + h: 2, + minW: 4, + minH: 2, + }, + MonthlyJobCosting: { + label: i18next.t("dashboard.titles.monthlyjobcosting"), + component: DashboardMonthlyJobCosting, + gqlFragment: null, + minW: 6, + minH: 3, + w: 6, + h: 3, + }, + MonthlyPartsSales: { + label: i18next.t("dashboard.titles.monthlypartssales"), + component: DashboardMonthlyPartsSales, + gqlFragment: null, + minW: 2, + minH: 2, + w: 2, + h: 2, + }, + MonthlyLaborSales: { + label: i18next.t("dashboard.titles.monthlylaborsales"), + component: DashboardMonthlyLaborSales, + gqlFragment: null, + minW: 2, + minH: 2, + w: 2, + h: 2, + }, + // Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings + MonthlyEmployeeEfficency: { + label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), + component: DashboardMonthlyEmployeeEfficiency, + gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, + minW: 2, + minH: 2, + w: 2, + h: 2, + }, + ScheduleInToday: { + label: i18next.t("dashboard.titles.scheduledintoday"), + component: DashboardScheduledInToday, + gqlFragment: DashboardScheduledInTodayGql, + minW: 6, + minH: 2, + w: 10, + h: 3, + }, + ScheduleOutToday: { + label: i18next.t("dashboard.titles.scheduledouttoday"), + component: DashboardScheduledOutToday, + gqlFragment: DashboardScheduledOutTodayGql, + minW: 6, + minH: 2, + w: 10, + 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 componentBasedAdditions = - state && - Array.isArray(state.layout) && - state.layout - .map((item, index) => componentList[item.i].gqlFragment || "") - .join(""); - return gql` - query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} - monthly_sales: jobs(where: {_and: [ - { voided: {_eq: false}}, - {date_invoiced: {_gte: "${moment() + const componentBasedAdditions = + state && + Array.isArray(state.layout) && + state.layout + .map((item, index) => componentList[item.i].gqlFragment || "") + .join(""); + return gql` + query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} + monthly_sales: jobs(where: {_and: [ + { voided: {_eq: false}}, + {date_invoiced: {_gte: "${moment() .startOf("month") - .startOf("day") - .toISOString()}"}}, {date_invoiced: {_lte: "${moment() + .startOf("day") + .toISOString()}"}}, {date_invoiced: {_lte: "${moment() .endOf("month") .endOf("day") .toISOString()}"}}]}) { id - ro_number - date_invoiced - job_totals - rate_la1 - rate_la2 - rate_la3 - rate_la4 - rate_laa - rate_lab - rate_lad - rate_lae - rate_laf - rate_lag - rate_lam - rate_lar - rate_las - rate_lau - rate_ma2s - rate_ma2t - rate_ma3s - rate_mabl - rate_macs - rate_mahw - rate_mapa - rate_mash - rate_matd - joblines(where: { removed: { _eq: false } }) { - id - mod_lbr_ty - mod_lb_hrs - act_price - part_qty - part_type - } - } - production_jobs: jobs(where: { inproduction: { _eq: true } }) { - id - ro_number - ins_co_nm - job_totals - joblines(where: { removed: { _eq: false } }) { - id - mod_lbr_ty - mod_lb_hrs - act_price - part_qty - part_type - } - labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } - } - } - larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } - } - } - } - }`; + ro_number + date_invoiced + job_totals + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_mapa + rate_mash + rate_matd + joblines(where: { removed: { _eq: false } }) { + id + mod_lbr_ty + mod_lb_hrs + act_price + part_qty + part_type + } + } + production_jobs: jobs(where: { inproduction: { _eq: true } }) { + id + ro_number + ins_co_nm + job_totals + joblines(where: { removed: { _eq: false } }) { + id + mod_lbr_ty + mod_lb_hrs + act_price + part_qty + part_type + } + labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + } + }`; }; diff --git a/client/src/components/job-detail-cards/job-detail-cards.totals.component.jsx b/client/src/components/job-detail-cards/job-detail-cards.totals.component.jsx index 31236a9e4..ddcdb2c68 100644 --- a/client/src/components/job-detail-cards/job-detail-cards.totals.component.jsx +++ b/client/src/components/job-detail-cards/job-detail-cards.totals.component.jsx @@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) { /> 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(); return ( @@ -93,7 +101,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? ( - {job.scheduled_in} + + {job.scheduled_in} + ) : null} @@ -299,6 +309,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { >
+ + + {bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "} + {(bodyHrs + refinishHrs).toFixed(1)} +
diff --git a/client/src/components/owner-detail-jobs/owner-detail-jobs.component.jsx b/client/src/components/owner-detail-jobs/owner-detail-jobs.component.jsx index 360954101..d47be4d1f 100644 --- a/client/src/components/owner-detail-jobs/owner-detail-jobs.component.jsx +++ b/client/src/components/owner-detail-jobs/owner-detail-jobs.component.jsx @@ -24,7 +24,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) { const handleTableChange = (pagination, filters, sorter) => { setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; - + const columns = [ { title: t("jobs.fields.ro_number"), @@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) { title: t("jobs.fields.vehicle"), dataIndex: "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) => record.vehicleid ? ( @@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) { title: t("jobs.fields.status"), dataIndex: "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: 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), }, { diff --git a/client/src/components/parts-queue-list/parts-queue.list.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx index 44d5368ce..511cade62 100644 --- a/client/src/components/parts-queue-list/parts-queue.list.component.jsx +++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx @@ -95,15 +95,13 @@ export function PartsQueueListComponent({ bodyshop }) { }; const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - history.push({ - search: queryString.stringify({ - ...searchParams, - selected: record.id, - }), - }); - } + if (record?.id) { + history.replace({ + search: queryString.stringify({ + ...searchParams, + selected: record.id, + }), + }); } }; @@ -350,6 +348,13 @@ export function PartsQueueListComponent({ bodyshop }) { selectedRowKeys: [selected], type: "radio", }} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); + }, + }; + }} /> ); diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js index 824203975..7e706e430 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.js +++ b/client/src/components/production-list-columns/production-list-columns.data.js @@ -1,7 +1,6 @@ import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons"; import { Checkbox, Space, Tooltip } from "antd"; import i18n from "i18next"; -import moment from "moment"; import { Link } from "react-router-dom"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { TimeFormatter } from "../../utils/DateFormatter"; @@ -190,17 +189,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => { state.sortedInfo.columnKey === "date_next_contact" && state.sortedInfo.order, render: (text, record) => ( - - - + ), }, { diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 75e85e88d..86ec27749 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -5,6 +5,7 @@ import { Input, InputNumber, Select, + Space, Switch, Typography, } from "antd"; @@ -17,6 +18,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { useTreatments } from "@splitsoftware/splitio-react"; +import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; const SelectorDiv = styled.div` .ant-form-item .ant-select { @@ -191,7 +193,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { - {(fields, { add, remove }) => { + {(fields, { add, remove, move }) => { return (
{fields.map((field, index) => ( @@ -249,11 +251,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { - { - remove(field.name); - }} - /> + + { + remove(field.name); + }} + /> + + ))} @@ -345,7 +354,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { id="costs" > - {(fields, { add, remove }) => { + {(fields, { add, remove, move }) => { return (
{fields.map((field, index) => ( @@ -462,12 +471,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - - { - remove(field.name); - }} - /> + + { + remove(field.name); + }} + /> + + ))} @@ -493,7 +508,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { id="profits" > - {(fields, { add, remove }) => { + {(fields, { add, remove, move }) => { return (
{fields.map((field, index) => ( @@ -595,11 +610,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - { - remove(field.name); - }} - /> + + { + remove(field.name); + }} + /> + + ))} diff --git a/client/src/components/vehicle-detail-jobs/vehicle-detail-jobs.component.jsx b/client/src/components/vehicle-detail-jobs/vehicle-detail-jobs.component.jsx index 403d223bf..370142a2a 100644 --- a/client/src/components/vehicle-detail-jobs/vehicle-detail-jobs.component.jsx +++ b/client/src/components/vehicle-detail-jobs/vehicle-detail-jobs.component.jsx @@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; 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"; const mapStateToProps = createStructuredSelector({ @@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) { title: t("jobs.fields.owner"), dataIndex: "owner", key: "owner", + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => ( @@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) { title: t("jobs.fields.status"), dataIndex: "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: 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), }, { diff --git a/client/src/components/vehicles-list/vehicles-list.component.jsx b/client/src/components/vehicles-list/vehicles-list.component.jsx index bb1a97183..8a6f9ea61 100644 --- a/client/src/components/vehicles-list/vehicles-list.component.jsx +++ b/client/src/components/vehicles-list/vehicles-list.component.jsx @@ -4,8 +4,9 @@ import queryString from "query-string"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useHistory, useLocation } from "react-router-dom"; +import { pageLimit } from "../../utils/config"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; -import {pageLimit} from "../../utils/config"; +import { alphaSort } from './../../utils/sorters'; export default function VehiclesListComponent({ loading, vehicles, @@ -31,6 +32,8 @@ export default function VehiclesListComponent({ title: t("vehicles.fields.v_vin"), dataIndex: "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) => ( {record.v_vin || "N/A"} @@ -51,8 +54,10 @@ export default function VehiclesListComponent({ }, { title: t("vehicles.fields.plate_no"), - dataIndex: "plate", - key: "plate", + dataIndex: "plate_no", + 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) => { return ( {`${record.plate_st || ""} | ${record.plate_no || ""}`} diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js index 7a65be083..9632ce148 100644 --- a/client/src/firebase/firebase.utils.js +++ b/client/src/firebase/firebase.utils.js @@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { store } from "../redux/store"; +import axios from "axios"; +import { checkBeta } from "../utils/handleBeta"; const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); initializeApp(config); @@ -86,6 +88,18 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => { null, ...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( // "%c[Analytics]", // "background-color: green ;font-weight:bold;", diff --git a/client/src/graphql/vehicles.queries.js b/client/src/graphql/vehicles.queries.js index 619ff8961..1daccc7f5 100644 --- a/client/src/graphql/vehicles.queries.js +++ b/client/src/graphql/vehicles.queries.js @@ -31,6 +31,7 @@ export const QUERY_VEHICLE_BY_ID = gql` jobs(order_by: { date_open: desc }) { id ro_number + ownr_co_nm ownr_fn ownr_ln owner { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 87bf0c22e..b70828672 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -259,6 +259,7 @@ "saving": "Error encountered while saving. {{message}}" }, "fields": { + "ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "address1": "Address 1", "address2": "Address 2", "appt_alt_transport": "Appointment Alternative Transportation Options", @@ -477,7 +478,6 @@ "editaccess": "Users -> Edit access" } }, - "ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "responsibilitycenter": "Responsibility Center", "responsibilitycenter_accountdesc": "Account Description", "responsibilitycenter_accountitem": "Item", @@ -608,7 +608,7 @@ "dms": { "cdk": { "controllist": "Control Number List", - "payers": "CDK Payers" + "payers": "Payers" }, "cdk_dealerid": "CDK Dealer ID", "pbs_serialnumber": "PBS Serial Number", @@ -844,8 +844,8 @@ "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.", "notfoundtitle": "No survey found.", - "surveycompletetitle": "Survey previously completed", - "surveycompletesubtitle": "This survey was already completed on {{date}}." + "surveycompletesubtitle": "This survey was already completed on {{date}}.", + "surveycompletetitle": "Survey previously completed" }, "fields": { "completedon": "Completed On", @@ -854,13 +854,13 @@ "validuntil": "Valid Until" }, "labels": { + "copyright": "Copyright © $t(titles.app). All Rights Reserved.", + "greeting": "Hi {{name}}!", + "intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.", "nologgedinuser": "Please log out of $t(titles.app)", "nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.", "noneselected": "No response selected.", - "title": "Customer Satisfaction Survey", - "greeting": "Hi {{name}}!", - "intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.", - "copyright": "Copyright © $t(titles.app). All Rights Reserved." + "title": "Customer Satisfaction Survey" }, "successes": { "created": "CSI created successfully.", @@ -898,7 +898,8 @@ "scheduledindate": "Sheduled In Today: {{date}}", "scheduledintoday": "Sheduled In Today", "scheduledoutdate": "Sheduled Out Today: {{date}}", - "scheduledouttoday": "Sheduled Out Today" + "scheduledouttoday": "Sheduled Out Today", + "joblifecycle": "Job Lifecycle" } }, "dms": { @@ -1236,7 +1237,15 @@ "relative_end": "Relative End", "relative_start": "Relative Start", "start": "Start", - "value": "Value" + "value": "Value", + "status": "Status", + "percentage": "Percentage", + "human_readable": "Human Readable", + "status_count": "In Status" + }, + "titles": { + "dashboard": "Job Lifecycle", + "top_durations": "Top Durations" }, "content": { "current_status_accumulated_time": "Current Status Accumulated Time", @@ -1248,7 +1257,9 @@ "title": "Job Lifecycle Component", "title_durations": "Historical Status Durations", "title_loading": "Loading", - "title_transitions": "Transitions" + "title_transitions": "Transitions", + "calculated_based_on": "Calculated based on", + "jobs_in_since": "Jobs in since" }, "errors": { "fetch": "Error getting Job Lifecycle Data" @@ -1823,6 +1834,7 @@ "job": "Job Details", "jobcosting": "Job Costing", "jobtotals": "Job Totals", + "labor_hrs": "B/P/T Hrs", "labor_rates_subtotal": "Labor Rates Subtotal", "laborallocations": "Labor Allocations", "labortotals": "Labor Totals", @@ -2417,6 +2429,7 @@ "invoice_total_payable": "Invoice (Total Payable)", "iou_form": "IOU Form", "job_costing_ro": "Job Costing", + "job_lifecycle_ro": "Job Lifecycle", "job_notes": "Job Notes", "key_tag": "Key Tag", "labels": { @@ -2583,17 +2596,17 @@ }, "labels": { "advanced_filters": "Advanced Filters and Sorters", - "advanced_filters_show": "Show", - "advanced_filters_hide": "Hide", - "advanced_filters_filters": "Filters", - "advanced_filters_sorters": "Sorters", - "advanced_filters_filter_field": "Field", - "advanced_filters_sorter_field": "Field", - "advanced_filters_true": "True", "advanced_filters_false": "False", - "advanced_filters_sorter_direction": "Direction", + "advanced_filters_filter_field": "Field", "advanced_filters_filter_operator": "Operator", "advanced_filters_filter_value": "Value", + "advanced_filters_filters": "Filters", + "advanced_filters_hide": "Hide", + "advanced_filters_show": "Show", + "advanced_filters_sorter_direction": "Direction", + "advanced_filters_sorter_field": "Field", + "advanced_filters_sorters": "Sorters", + "advanced_filters_true": "True", "dates": "Dates", "employee": "Employee", "filterson": "Filters on {{object}}: {{field}}", @@ -2675,6 +2688,8 @@ "job_costing_ro_date_summary": "Job Costing by RO - Summary", "job_costing_ro_estimator": "Job Costing by Estimator", "job_costing_ro_ins_co": "Job Costing by RO Source", + "job_lifecycle_date_detail": "Job Lifecycle by Date - Detail", + "job_lifecycle_date_summary": "Job Lifecycle by Date - Summary", "jobs_completed_not_invoiced": "Jobs Completed not Invoiced", "jobs_invoiced_not_exported": "Jobs Invoiced not Exported", "jobs_reconcile": "Parts/Sublet/Labor Reconciliation", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index c8dd488ab..d1a760146 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -259,6 +259,7 @@ "saving": "" }, "fields": { + "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", @@ -477,7 +478,6 @@ "editaccess": "" } }, - "ReceivableCustomField": "", "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -844,8 +844,8 @@ "notconfigured": "", "notfoundsubtitle": "", "notfoundtitle": "", - "surveycompletetitle": "", - "surveycompletesubtitle": "" + "surveycompletesubtitle": "", + "surveycompletetitle": "" }, "fields": { "completedon": "", @@ -854,13 +854,13 @@ "validuntil": "" }, "labels": { + "copyright": "", + "greeting": "", + "intro": "", "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "", - "greeting": "", - "intro": "", - "copyright": "" + "title": "" }, "successes": { "created": "", @@ -898,7 +898,8 @@ "scheduledindate": "", "scheduledintoday": "", "scheduledoutdate": "", - "scheduledouttoday": "" + "scheduledouttoday": "", + "joblifecycle": "" } }, "dms": { @@ -1236,7 +1237,15 @@ "relative_end": "", "relative_start": "", "start": "", - "value": "" + "value": "", + "status": "", + "percentage": "", + "human_readable": "", + "status_count": "" + }, + "titles": { + "dashboard": "", + "top_durations": "" }, "content": { "current_status_accumulated_time": "", @@ -1248,7 +1257,9 @@ "title": "", "title_durations": "", "title_loading": "", - "title_transitions": "" + "title_transitions": "", + "calculated_based_on": "", + "jobs_in_since": "" }, "errors": { "fetch": "Error al obtener los datos del ciclo de vida del trabajo" @@ -1823,6 +1834,7 @@ "job": "", "jobcosting": "", "jobtotals": "", + "labor_hrs": "", "labor_rates_subtotal": "", "laborallocations": "", "labortotals": "", @@ -2417,6 +2429,7 @@ "invoice_total_payable": "", "iou_form": "", "job_costing_ro": "", + "job_lifecycle_ro": "", "job_notes": "", "key_tag": "", "labels": { @@ -2583,17 +2596,17 @@ }, "labels": { "advanced_filters": "", - "advanced_filters_show": "", - "advanced_filters_hide": "", - "advanced_filters_filters": "", - "advanced_filters_sorters": "", - "advanced_filters_filter_field": "", - "advanced_filters_sorter_field": "", - "advanced_filters_true": "", "advanced_filters_false": "", - "advanced_filters_sorter_direction": "", + "advanced_filters_filter_field": "", "advanced_filters_filter_operator": "", "advanced_filters_filter_value": "", + "advanced_filters_filters": "", + "advanced_filters_hide": "", + "advanced_filters_show": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorters": "", + "advanced_filters_true": "", "dates": "", "employee": "", "filterson": "", @@ -2675,6 +2688,8 @@ "job_costing_ro_date_summary": "", "job_costing_ro_estimator": "", "job_costing_ro_ins_co": "", + "job_lifecycle_date_detail": "", + "job_lifecycle_date_summary": "", "jobs_completed_not_invoiced": "", "jobs_invoiced_not_exported": "", "jobs_reconcile": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index d64b315ef..87253cf17 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -259,6 +259,7 @@ "saving": "" }, "fields": { + "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", @@ -477,7 +478,6 @@ "editaccess": "" } }, - "ReceivableCustomField": "", "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -844,8 +844,8 @@ "notconfigured": "", "notfoundsubtitle": "", "notfoundtitle": "", - "surveycompletetitle": "", - "surveycompletesubtitle": "" + "surveycompletesubtitle": "", + "surveycompletetitle": "" }, "fields": { "completedon": "", @@ -854,13 +854,13 @@ "validuntil": "" }, "labels": { + "copyright": "", + "greeting": "", + "intro": "", "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "", - "greeting": "", - "intro": "", - "copyright": "" + "title": "" }, "successes": { "created": "", @@ -1236,7 +1236,15 @@ "relative_end": "", "relative_start": "", "start": "", - "value": "" + "value": "", + "status": "", + "percentage": "", + "human_readable": "", + "status_count": "" + }, + "titles": { + "dashboard": "", + "top_durations": "" }, "content": { "current_status_accumulated_time": "", @@ -1248,7 +1256,10 @@ "title": "", "title_durations": "", "title_loading": "", - "title_transitions": "" + "title_transitions": "", + "calculated_based_on": "", + "jobs_in_since": "", + "joblifecycle": "" }, "errors": { "fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches" @@ -1823,6 +1834,7 @@ "job": "", "jobcosting": "", "jobtotals": "", + "labor_hrs": "", "labor_rates_subtotal": "", "laborallocations": "", "labortotals": "", @@ -2417,6 +2429,7 @@ "invoice_total_payable": "", "iou_form": "", "job_costing_ro": "", + "job_lifecycle_ro": "", "job_notes": "", "key_tag": "", "labels": { @@ -2583,17 +2596,17 @@ }, "labels": { "advanced_filters": "", - "advanced_filters_show": "", - "advanced_filters_hide": "", - "advanced_filters_filters": "", - "advanced_filters_sorters": "", - "advanced_filters_filter_field": "", - "advanced_filters_sorter_field": "", - "advanced_filters_true": "", "advanced_filters_false": "", - "advanced_filters_sorter_direction": "", + "advanced_filters_filter_field": "", "advanced_filters_filter_operator": "", "advanced_filters_filter_value": "", + "advanced_filters_filters": "", + "advanced_filters_hide": "", + "advanced_filters_show": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorters": "", + "advanced_filters_true": "", "dates": "", "employee": "", "filterson": "", @@ -2675,6 +2688,8 @@ "job_costing_ro_date_summary": "", "job_costing_ro_estimator": "", "job_costing_ro_ins_co": "", + "job_lifecycle_date_detail": "", + "job_lifecycle_date_summary": "", "jobs_completed_not_invoiced": "", "jobs_invoiced_not_exported": "", "jobs_reconcile": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index bb6c23731..e5a762852 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -514,6 +514,14 @@ export const TemplateList = (type, context) => { group: "financial", dms: 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" @@ -2048,6 +2056,30 @@ export const TemplateList = (type, context) => { datedisable: true, 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" diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 587fa8ac3..0467fae39 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4200,7 +4200,7 @@ interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook_from_env: HASURA_API_URL + webhook: https://worktest.home.irony.online headers: - name: event-secret value_from_env: EVENT_SECRET diff --git a/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/down.sql b/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/down.sql new file mode 100644 index 000000000..c15a1de62 --- /dev/null +++ b/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/down.sql @@ -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; diff --git a/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/up.sql b/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/up.sql new file mode 100644 index 000000000..52debb0eb --- /dev/null +++ b/hasura/migrations/1710520532230_alter_table_public_ioevents_add_column_useremail/up.sql @@ -0,0 +1 @@ +alter table "public"."ioevents" add column "useremail" text; diff --git a/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/down.sql b/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/down.sql new file mode 100644 index 000000000..ce3de08a7 --- /dev/null +++ b/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/down.sql @@ -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; diff --git a/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/up.sql b/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/up.sql new file mode 100644 index 000000000..ac3ff14ab --- /dev/null +++ b/hasura/migrations/1710520551755_alter_table_public_ioevents_add_column_bodyshopid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."ioevents" add column "bodyshopid" uuid + null; diff --git a/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/down.sql b/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/down.sql new file mode 100644 index 000000000..5c6a0fcaf --- /dev/null +++ b/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/down.sql @@ -0,0 +1 @@ +alter table "public"."ioevents" alter column "useremail" set not null; diff --git a/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/up.sql b/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/up.sql new file mode 100644 index 000000000..12a8aaaab --- /dev/null +++ b/hasura/migrations/1710520563676_alter_table_public_ioevents_alter_column_useremail/up.sql @@ -0,0 +1 @@ +alter table "public"."ioevents" alter column "useremail" drop not null; diff --git a/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/down.sql b/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/down.sql new file mode 100644 index 000000000..a70c9031c --- /dev/null +++ b/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/down.sql @@ -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; diff --git a/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/up.sql b/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/up.sql new file mode 100644 index 000000000..21077366d --- /dev/null +++ b/hasura/migrations/1710520583458_alter_table_public_ioevents_add_column_env/up.sql @@ -0,0 +1,2 @@ +alter table "public"."ioevents" add column "env" text + null; diff --git a/hasura/migrations/1710520629475_create_index_ioevents_useremail/down.sql b/hasura/migrations/1710520629475_create_index_ioevents_useremail/down.sql new file mode 100644 index 000000000..413d340c9 --- /dev/null +++ b/hasura/migrations/1710520629475_create_index_ioevents_useremail/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."ioevents_useremail"; diff --git a/hasura/migrations/1710520629475_create_index_ioevents_useremail/up.sql b/hasura/migrations/1710520629475_create_index_ioevents_useremail/up.sql new file mode 100644 index 000000000..d9a5a1a2c --- /dev/null +++ b/hasura/migrations/1710520629475_create_index_ioevents_useremail/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "ioevents_useremail" on + "public"."ioevents" using btree ("useremail"); diff --git a/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/down.sql b/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/down.sql new file mode 100644 index 000000000..33632f335 --- /dev/null +++ b/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/down.sql @@ -0,0 +1 @@ +alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey"; diff --git a/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/up.sql b/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/up.sql new file mode 100644 index 000000000..47ac7307c --- /dev/null +++ b/hasura/migrations/1710520654372_set_fk_public_ioevents_useremail/up.sql @@ -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; diff --git a/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/down.sql b/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/down.sql new file mode 100644 index 000000000..aa868e98e --- /dev/null +++ b/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/down.sql @@ -0,0 +1 @@ +alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey"; diff --git a/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/up.sql b/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/up.sql new file mode 100644 index 000000000..e11c5e9b9 --- /dev/null +++ b/hasura/migrations/1710520693899_set_fk_public_ioevents_bodyshopid/up.sql @@ -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; diff --git a/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/down.sql b/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/down.sql new file mode 100644 index 000000000..65e575e43 --- /dev/null +++ b/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_audit_trail_type"; diff --git a/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/up.sql b/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/up.sql new file mode 100644 index 000000000..70e81381d --- /dev/null +++ b/hasura/migrations/1710523428339_create_index_idx_audit_trail_type/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_audit_trail_type" on + "public"."audit_trail" using btree ("type"); diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 0d3720d25..902c0b4cf 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -201,19 +201,24 @@ exports.default = async (req, res) => { } 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 - )} - `, - }); + // 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))}`, + }); } }; diff --git a/server/ioevent/ioevent.js b/server/ioevent/ioevent.js index 4ad524eb8..967e1c7ad 100644 --- a/server/ioevent/ioevent.js +++ b/server/ioevent/ioevent.js @@ -11,27 +11,40 @@ require("dotenv").config({ }); 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 { - // await client.request(queries.INSERT_IOEVENT, { - // event: { - // operationname: operationName, - // time, - // dbevent, - // }, - // }); - console.log("IOEVENT", operationName, time, dbevent, user, imexshopid); - logger.log("ioevent", "trace", user, null, { - imexshopid, - operationName, - time, - dbevent, + await client.request(queries.INSERT_IOEVENT, { + event: { + operationname: operationName, + time, + dbevent, + env, + variables, + bodyshopid, + useremail, + }, }); - res.sendStatus(200); } catch (error) { - console.log("error", error); - res.status(400).send(error); + logger.log("ioevent-error", "trace", user, null, { + operationname: operationName, + time, + dbevent, + env, + variables, + bodyshopid, + useremail, + }); + res.sendStatus(200); } }; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index c2ea72737..b94f9ab68 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -3,6 +3,7 @@ const queries = require("../graphql-client/queries"); const moment = require("moment"); const durationToHumanReadable = require("../utils/durationToHumanReadable"); const calculateStatusDuration = require("../utils/calculateStatusDuration"); +const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor"); const jobLifecycle = async (req, res) => { // Grab the jobids and statuses from the request body @@ -28,12 +29,12 @@ const jobLifecycle = async (req, res) => { jobIDs, transitions: [] }); - } const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; + const allDurations = []; for (let jobId in transitionsByJobId) { let lifecycle = transitionsByJobId[jobId].map(transition => { @@ -53,15 +54,57 @@ const jobLifecycle = async (req, res) => { return transition; }); + const durations = calculateStatusDuration(lifecycle, statuses); + groupedTransitions[jobId] = { - lifecycle: lifecycle, - durations: calculateStatusDuration(lifecycle, statuses), + lifecycle, + 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({ jobIDs, transition: groupedTransitions, + durations: { + jobs: jobIDs.length, + summations: finalSummations, + totalStatuses: finalSummations.length, + total: finalTotal, + statusCounts: finalStatusCounts, + humanReadable: durationToHumanReadable(moment.duration(finalTotal)) + } }); } diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index 16165d001..1fcdcc487 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -1,15 +1,7 @@ const durationToHumanReadable = require("./durationToHumanReadable"); const moment = require("moment"); +const getLifecycleStatusColor = require("./getLifecycleStatusColor"); 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) => { let statusDuration = {}; @@ -33,26 +25,16 @@ const calculateStatusDuration = (transitions, statuses) => { if (!transition.prev_value) { statusDuration[transition.value] = { value: duration, - humanReadable: transition.duration_readable + humanReadable: durationToHumanReadable(moment.duration(duration)) }; - } else if (!transition.next_value) { + } else { if (statusDuration[transition.value]) { statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; + statusDuration[transition.value].humanReadable = durationToHumanReadable(moment.duration(statusDuration[transition.value].value)); } else { statusDuration[transition.value] = { value: duration, - humanReadable: transition.duration_readable - }; - } - } 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 + humanReadable: durationToHumanReadable(moment.duration(duration)) }; } } @@ -79,7 +61,7 @@ const calculateStatusDuration = (transitions, statuses) => { value, humanReadable, percentage: statusDuration[status].percentage, - color: getColor(status), + color: getLifecycleStatusColor(status), roundedPercentage: `${Math.round(statusDuration[status].percentage)}%` }); } diff --git a/server/utils/getLifecycleStatusColor.js b/server/utils/getLifecycleStatusColor.js new file mode 100644 index 000000000..d726e1cb3 --- /dev/null +++ b/server/utils/getLifecycleStatusColor.js @@ -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; \ No newline at end of file