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..123907591 --- /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 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 {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: "${dayjs().subtract(45, 'day').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 3b21370ca..ffa9a8f7a 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -42,6 +42,9 @@ import DashboardScheduledInToday, { import DashboardScheduledOutToday, { 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"; @@ -260,6 +263,7 @@ const componentList = { 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, @@ -287,6 +291,15 @@ const componentList = { 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) => { 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 e1fdff5f2..3dc014ac4 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 ( @@ -89,7 +97,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} @@ -295,6 +305,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 d75dd5bc3..c65bd34f8 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 @@ -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 e26773598..3b0aa9087 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 @@ -100,8 +100,7 @@ export function PartsQueueListComponent({bodyshop}) { }; const handleOnRowClick = (record) => { - if (record) { - if (record.id) { + if (record?.id) { history({ search: queryString.stringify({ ...searchParams, @@ -109,7 +108,6 @@ export function PartsQueueListComponent({bodyshop}) { }), }); } - } }; const columns = [ { @@ -354,7 +352,15 @@ 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 6809bfbe1..bde479bbc 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 dayjs from "../../utils/day"; import {Link} from "react-router-dom"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import {TimeFormatter} from "../../utils/DateFormatter"; @@ -189,17 +188,12 @@ const r = ({technician, state, activeStatuses, data, bodyshop, refetch}) => { 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 f626b60ec..26211e739 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -1,5 +1,5 @@ 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 {useTranslation} from "react-i18next"; import styled from "styled-components"; @@ -9,6 +9,7 @@ import {selectBodyshop} from "../../redux/user/user.selectors"; import {connect} from "react-redux"; import {createStructuredSelector} from "reselect"; import {useSplitTreatments} 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 { @@ -180,7 +181,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { - {(fields, {add, remove}) => { + {(fields, {add, remove, move}) => { return (
{fields.map((field, index) => ( @@ -238,11 +239,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { - { - remove(field.name); - }} - /> + + d + onClick={() => { + remove(field.name); + }} + /> + + ))} @@ -334,7 +342,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { id="costs" > - {(fields, {add, remove}) => { + {(fields, {add, remove, move}) => { return (
{fields.map((field, index) => ( @@ -451,12 +459,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { )} - - { - remove(field.name); - }} - /> + + { + remove(field.name); + }} + /> + + ))} @@ -482,7 +496,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { id="profits" > - {(fields, {add, remove}) => { + {(fields, {add, remove, move}) => { return (
{fields.map((field, index) => ( @@ -584,11 +598,18 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { )} - { - remove(field.name); - }} - /> + + { + remove(field.name); + }} + /> + + ))} @@ -613,7 +634,7 @@ export function ShopInfoResponsibilityCenterComponent({bodyshop, form}) { {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( <> - {(fields, {add, remove}) => { + {(fields, {add, remove, move}) => { return (
{fields.map((field, index) => ( 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 4237ade41..6fe616d09 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 @@ -6,8 +6,10 @@ import {Link} from "react-router-dom"; 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 { alphaSort, statusSort } from "../../utils/sorters"; +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 82c1123a4..a5f8bf982 100644 --- a/client/src/components/vehicles-list/vehicles-list.component.jsx +++ b/client/src/components/vehicles-list/vehicles-list.component.jsx @@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next"; import {Link, useLocation, useNavigate} from "react-router-dom"; 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, @@ -32,6 +33,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"} @@ -52,8 +55,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 1d13a2312..0d6ba4708 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,17 @@ 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 742c4de36..32d970655 100644 --- a/client/src/graphql/vehicles.queries.js +++ b/client/src/graphql/vehicles.queries.js @@ -1,47 +1,48 @@ import {gql} from "@apollo/client"; export const QUERY_VEHICLE_BY_ID = gql` - query QUERY_VEHICLE_BY_ID($id: uuid!) { - vehicles_by_pk(id: $id) { - created_at - db_v_code - id - plate_no - plate_st - v_vin - v_type - v_trimcode - v_tone - v_stage - v_prod_dt - v_paint_codes - v_options - v_model_yr - v_model_desc - v_mldgcode - v_makecode - v_make_desc - v_engine - v_cond - v_color - v_bstyle - updated_at - trim_color - notes - jobs(order_by: { date_open: desc }) { - id - ro_number - ownr_fn - ownr_ln - owner { - id - } - clm_no - status - clm_total - } + query QUERY_VEHICLE_BY_ID($id: uuid!) { + vehicles_by_pk(id: $id) { + created_at + db_v_code + id + plate_no + plate_st + v_vin + v_type + v_trimcode + v_tone + v_stage + v_prod_dt + v_paint_codes + v_options + v_model_yr + v_model_desc + v_mldgcode + v_makecode + v_make_desc + v_engine + v_cond + v_color + v_bstyle + updated_at + trim_color + notes + jobs(order_by: { date_open: desc }) { + id + ro_number + ownr_co_nm + ownr_fn + ownr_ln + owner { + id } + clm_no + status + clm_total + } } + } `; export const UPDATE_VEHICLE = gql` diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index ddd7dbc11..204d3de15 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -257,6 +257,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", @@ -475,7 +476,7 @@ "editaccess": "Users -> Edit access" } }, - "ReceivableCustomField": "QBO Receivable Custom Field {{number}}", + "responsibilitycenter": "Responsibility Center", "responsibilitycenter_accountdesc": "Account Description", "responsibilitycenter_accountitem": "Item", @@ -606,7 +607,7 @@ "dms": { "cdk": { "controllist": "Control Number List", - "payers": "CDK Payers" + "payers": " Payers" }, "cdk_dealerid": "CDK Dealer ID", "pbs_serialnumber": "PBS Serial Number", @@ -842,8 +843,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", @@ -852,13 +853,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. ", @@ -896,7 +897,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": { @@ -1269,7 +1271,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", @@ -1281,7 +1291,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" @@ -1856,6 +1868,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", @@ -2450,6 +2463,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": { @@ -2616,17 +2630,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}}", @@ -2708,6 +2722,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 ea403b6ed..8a779eae9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -258,6 +258,7 @@ "saving": "" }, "fields": { + "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", @@ -476,7 +477,7 @@ "editaccess": "" } }, - "ReceivableCustomField": "", + "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -843,8 +844,8 @@ "notconfigured": "", "notfoundsubtitle": "", "notfoundtitle": "", - "surveycompletetitle": "", - "surveycompletesubtitle": "" + "surveycompletesubtitle": "", + "surveycompletetitle": "" }, "fields": { "completedon": "", @@ -853,13 +854,13 @@ "validuntil": "" }, "labels": { + "copyright": "", + "greeting": "", + "intro": "", "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "", - "greeting": "", - "intro": "", - "copyright": "" + "title": "" }, "successes": { "created": "", @@ -897,7 +898,8 @@ "scheduledindate": "", "scheduledintoday": "", "scheduledoutdate": "", - "scheduledouttoday": "" + "scheduledouttoday": "", + "joblifecycle": "" } }, "dms": { @@ -1270,7 +1272,15 @@ "relative_end": "", "relative_start": "", "start": "", - "value": "" + "value": "", + "status": "", + "percentage": "", + "human_readable": "", + "status_count": "" + }, + "titles": { + "dashboard": "", + "top_durations": "" }, "content": { "current_status_accumulated_time": "", @@ -1282,7 +1292,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" @@ -1857,6 +1869,7 @@ "job": "", "jobcosting": "", "jobtotals": "", + "labor_hrs": "", "labor_rates_subtotal": "", "laborallocations": "", "labortotals": "", @@ -2451,6 +2464,7 @@ "invoice_total_payable": "", "iou_form": "", "job_costing_ro": "", + "job_lifecycle_ro": "", "job_notes": "", "key_tag": "", "labels": { @@ -2617,17 +2631,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": "", @@ -2709,6 +2723,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 569b13e3e..5a28a5ea6 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -258,6 +258,7 @@ "saving": "" }, "fields": { + "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", @@ -476,7 +477,7 @@ "editaccess": "" } }, - "ReceivableCustomField": "", + "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -843,8 +844,8 @@ "notconfigured": "", "notfoundsubtitle": "", "notfoundtitle": "", - "surveycompletetitle": "", - "surveycompletesubtitle": "" + "surveycompletesubtitle": "", + "surveycompletetitle": "" }, "fields": { "completedon": "", @@ -853,13 +854,13 @@ "validuntil": "" }, "labels": { + "copyright": "", + "greeting": "", + "intro": "", "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "", - "greeting": "", - "intro": "", - "copyright": "" + "title": "" }, "successes": { "created": "", @@ -1270,7 +1271,15 @@ "relative_end": "", "relative_start": "", "start": "", - "value": "" + "value": "", + "status": "", + "percentage": "", + "human_readable": "", + "status_count": "" + }, + "titles": { + "dashboard": "", + "top_durations": "" }, "content": { "current_status_accumulated_time": "", @@ -1282,7 +1291,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" @@ -1857,6 +1869,7 @@ "job": "", "jobcosting": "", "jobtotals": "", + "labor_hrs": "", "labor_rates_subtotal": "", "laborallocations": "", "labortotals": "", @@ -2451,6 +2464,7 @@ "invoice_total_payable": "", "iou_form": "", "job_costing_ro": "", + "job_lifecycle_ro": "", "job_notes": "", "key_tag": "", "labels": { @@ -2617,17 +2631,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": "", @@ -2709,6 +2723,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 019b8d4e5..e06f2f003 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 ae375e361..9cc14dd54 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 19f8d75a3..d6dc1c7eb 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -193,28 +193,33 @@ exports.default = async (req, res) => { }); } - //***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); + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml } 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) => { diff --git a/server/ioevent/ioevent.js b/server/ioevent/ioevent.js index bab241bbd..46d5564ba 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