diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3ee30f568..042ad7c33 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -3753,6 +3753,27 @@ + + md_jobline_presets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_payment_types false @@ -10532,6 +10553,27 @@ errors + + refreshrequired + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + updatinglayout false @@ -15803,6 +15845,27 @@ + + presets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -24413,6 +24476,27 @@ + + newjob + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + owners false diff --git a/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx b/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx index c47afbe1f..a8d100e02 100644 --- a/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx +++ b/client/src/components/dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx @@ -32,13 +32,9 @@ export default function DashboardMonthlyLaborSales({ data, ...cardProps }) { return { name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`), value: laborData[key].getAmount() / 100, - // color: pieColor(i.status), + color: pieColor(key.toUpperCase()), }; }); - console.log( - "🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData", - chartData - ); return ( @@ -72,36 +68,56 @@ export const DashboardMonthlyRevenueGraphGql = ` `; +const pieColor = (type) => { + if (type === "LAA") return "lightgreen"; + else if (type === "LAB") return "dodgerblue"; + else if (type === "LAD") return "aliceblue"; + else if (type === "LAE") return "seafoam"; + else if (type === "LAG") return "chartreuse"; + else if (type === "LAF") return "magenta"; + else if (type === "LAM") return "gold"; + else if (type === "LAR") return "crimson"; + else if (type === "LAU") return "slategray"; + else if (type === "LA1") return "slategray"; + else if (type === "LA2") return "slategray"; + else if (type === "LA3") return "slategray"; + else if (type === "LA4") return "slategray"; + return "slategray"; +}; + const renderActiveShape = (props) => { - const RADIAN = Math.PI / 180; + //const RADIAN = Math.PI / 180; const { cx, cy, - midAngle, + //midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, - percent, + // percent, value, } = props; - const sin = Math.sin(-RADIAN * midAngle); - const cos = Math.cos(-RADIAN * midAngle); - const sx = cx + (outerRadius + 10) * cos; - const sy = cy + (outerRadius + 10) * sin; - const mx = cx + (outerRadius + 30) * cos; - const my = cy + (outerRadius + 30) * sin; - const ex = mx + (cos >= 0 ? 1 : -1) * 22; - const ey = my; - const textAnchor = cos >= 0 ? "start" : "end"; + // const sin = Math.sin(-RADIAN * midAngle); + // const cos = Math.cos(-RADIAN * midAngle); + // // const sx = cx + (outerRadius + 10) * cos; + // const sy = cy + (outerRadius + 10) * sin; + // const mx = cx + (outerRadius + 30) * cos; + // const my = cy + (outerRadius + 30) * sin; + // //const ex = mx + (cos >= 0 ? 1 : -1) * 22; + // const ey = my; + //const textAnchor = cos >= 0 ? "start" : "end"; return ( - + {payload.name} + + {Dinero({ amount: Math.round(value * 100) }).toFormat()} + { outerRadius={outerRadius + 10} fill={fill} /> - - - = 0 ? 1 : -1) * 12} - y={ey} - textAnchor={textAnchor} - fill="#333" - > - {payload.name} - - = 0 ? 1 : -1) * 12} - y={ey} - dy={18} - textAnchor={textAnchor} - fill="#999" - > - {Dinero({ amount: Math.round(value * 100) }).toFormat()} - ); }; +// ; +// = 0 ? 1 : -1) * 12} +// y={ey} +// textAnchor={textAnchor} +// fill="#333" +// > +// {payload.name} +// +// = 0 ? 1 : -1) * 12} +// y={ey} +// dy={18} +// textAnchor={textAnchor} +// fill="#999" +// > +// {Dinero({ amount: Math.round(value * 100) }).toFormat()} +// diff --git a/client/src/components/dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx b/client/src/components/dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx index 9f651c009..7dfc6dc47 100644 --- a/client/src/components/dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx +++ b/client/src/components/dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx @@ -29,13 +29,9 @@ export default function DashboardMonthlyPartsSales({ data, ...cardProps }) { return { name: t(`joblines.fields.part_types.${key.toUpperCase()}`), value: partData[key].getAmount() / 100, - // color: pieColor(i.status), + color: pieColor(key.toUpperCase()), }; }); - console.log( - "🚀 ~ file: monthly-parts-sales.component.jsx ~ line 34 ~ chartData", - chartData - ); return ( @@ -68,37 +64,55 @@ export default function DashboardMonthlyPartsSales({ data, ...cardProps }) { export const DashboardMonthlyRevenueGraphGql = ` `; +const pieColor = (type) => { + if (type === "PAA") return "darkgreen"; + else if (type === "PAC") return "green"; + else if (type === "PAE") return "gold"; + else if (type === "PAG") return "seafoam"; + else if (type === "PAL") return "chartreuse"; + else if (type === "PAM") return "magenta"; + else if (type === "PAN") return "crimson"; + else if (type === "PAO") return "gold"; + else if (type === "PAP") return "crimson"; + else if (type === "PAR") return "indigo"; + else if (type === "PAS") return "dodgerblue"; + else if (type === "PASL") return "dodgerblue"; + return "slategray"; +}; const renderActiveShape = (props) => { - const RADIAN = Math.PI / 180; + // const RADIAN = Math.PI / 180; const { cx, cy, - midAngle, + // midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload, - percent, + // percent, value, } = props; - const sin = Math.sin(-RADIAN * midAngle); - const cos = Math.cos(-RADIAN * midAngle); - const sx = cx + (outerRadius + 10) * cos; - const sy = cy + (outerRadius + 10) * sin; - const mx = cx + (outerRadius + 30) * cos; - const my = cy + (outerRadius + 30) * sin; - const ex = mx + (cos >= 0 ? 1 : -1) * 22; - const ey = my; - const textAnchor = cos >= 0 ? "start" : "end"; + // const sin = Math.sin(-RADIAN * midAngle); + // const cos = Math.cos(-RADIAN * midAngle); + // const sx = cx + (outerRadius + 10) * cos; + //const sy = cy + (outerRadius + 10) * sin; + // const mx = cx + (outerRadius + 30) * cos; + //const my = cy + (outerRadius + 30) * sin; + // const ex = mx + (cos >= 0 ? 1 : -1) * 22; + // const ey = my; + // const textAnchor = cos >= 0 ? "start" : "end"; return ( - + {payload.name} + + {Dinero({ amount: Math.round(value * 100) }).toFormat()} + { outerRadius={outerRadius + 10} fill={fill} /> - - - = 0 ? 1 : -1) * 12} - y={ey} - textAnchor={textAnchor} - fill="#333" - > - {payload.name} - - = 0 ? 1 : -1) * 12} - y={ey} - dy={18} - textAnchor={textAnchor} - fill="#999" - > - {Dinero({ amount: Math.round(value * 100) }).toFormat()} - ); }; diff --git a/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx index 4cc24d59c..012bee464 100644 --- a/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx +++ b/client/src/components/dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx @@ -69,8 +69,8 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) { type="monotone" name="Accumulated Sales" dataKey="accSales" - fill="#8884d8" - stroke="#8884d8" + fill="#3CB371" + stroke="#3CB371" /> - - - {t("dashboard.errors.refreshrequired")} - + +
{t("dashboard.errors.refreshrequired")}
); diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 910913168..b7fa83049 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -198,15 +198,19 @@ const componentList = { label: "Production Dollars", component: DashboardTotalProductionDollars, gqlFragment: null, - w: 2, + w: 1, h: 1, + minW: 2, + minH: 1, }, ProductionHours: { label: "Production Hours", component: DashboardTotalProductionHours, gqlFragment: DashboardTotalProductionHoursGql, - w: 2, + w: 3, h: 1, + minW: 3, + minH: 1, }, ProjectedMonthlySales: { label: "Projected Monthly Sales", @@ -214,6 +218,8 @@ const componentList = { gqlFragment: DashboardProjectedMonthlySalesGql, w: 2, h: 1, + minW: 2, + minH: 1, }, MonthlyRevenueGraph: { label: "Monthly Sales Graph", @@ -254,12 +260,15 @@ const componentList = { }; const createDashboardQuery = (state) => { - const componentBasedAdditions = state.layout - .map((item, index) => componentList[item.i].gqlFragment || "") - .join(""); + const componentBasedAdditions = + state && + Array.isArray(state.layout) && + state.layout + .map((item, index) => componentList[item.i].gqlFragment || "") + .join(""); return gql` query QUERY_DASHBOARD_DETAILS { - ${componentBasedAdditions} + ${componentBasedAdditions || ""} monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment() .startOf("month") .format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment() diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 2c3887891..5069e3df6 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -1,4 +1,5 @@ import Icon, { + FileAddOutlined, BankFilled, BarChartOutlined, CarFilled, @@ -111,12 +112,14 @@ function Header({ {t("menus.header.availablejobs")} + }> + {t("menus.header.newjob")} + }> {t("menus.header.alljobs")} - }> {t("menus.header.productionlist")} @@ -128,7 +131,6 @@ function Header({ - }> {t("menus.header.scoreboard")} diff --git a/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx b/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx new file mode 100644 index 000000000..8e0aa2cb9 --- /dev/null +++ b/client/src/components/job-lines-preset-button/job-lines-preset-button.component.jsx @@ -0,0 +1,52 @@ +import { DownOutlined } from "@ant-design/icons"; +import { Dropdown, Menu } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function JoblinePresetButton({ bodyshop, form }) { + const { t } = useTranslation(); + + const handleSelect = (item) => { + form.setFieldsValue(item); + }; + + const menu = ( + + {bodyshop.md_jobline_presets.map((i, idx) => ( + handleSelect(i)} onItemHover key={idx}> + {i.label} + + ))} + + ); + + return ( + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(JoblinePresetButton); diff --git a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx index 308caf54e..520154f08 100644 --- a/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx +++ b/client/src/components/job-lines-upsert-modal/job-lines-upsert-modal.component.jsx @@ -3,7 +3,7 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import InputCurrency from "../form-items-formatted/currency-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; - +import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component"; export default function JobLinesUpsertModalComponent({ visible, jobLine, @@ -32,6 +32,7 @@ export default function JobLinesUpsertModalComponent({ onOk={() => form.submit()} okButtonProps={{ loading: loading }} onCancel={handleCancel} + e >
+ + + + diff --git a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.container.jsx b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.container.jsx index b95d81274..379fc5638 100644 --- a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.container.jsx +++ b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.container.jsx @@ -1,14 +1,14 @@ +import { useQuery } from "@apollo/client"; import React, { useContext } from "react"; -import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component"; +import { SEARCH_VEHICLES } from "../../graphql/vehicles.queries"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import AlertComponent from "../alert/alert.component"; -import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries"; -import { useQuery } from "@apollo/client"; +import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component"; export default function JobsCreateVehicleInfoContainer({ form }) { const [state] = useContext(JobCreateContext); - const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, { - variables: { vin: `%${state.vehicle.search}%` }, + const { loading, error, data } = useQuery(SEARCH_VEHICLES, { + variables: { search: `%${state.vehicle.search}%` }, skip: !state.vehicle.search, }); @@ -17,7 +17,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) { return ( ); } diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 9123eddfc..d8bd13647 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -825,6 +825,180 @@ export default function ShopInfoGeneral({ form }) { }} + + + {(fields, { add, remove, move }) => { + return ( +
+ {fields.map((field, index) => ( + + + + + + + + + + + + + + + + {" "} + + + + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + + ))} + + + +
+ ); + }} +
+
); } diff --git a/client/src/graphql/appointments.queries.js b/client/src/graphql/appointments.queries.js index 8afa5edfd..b5ffa9091 100644 --- a/client/src/graphql/appointments.queries.js +++ b/client/src/graphql/appointments.queries.js @@ -228,7 +228,6 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql` where: { scheduled_completion: { _gte: $start, _lte: $end } } ) { id - ro_number scheduled_completion labhrs: joblines_aggregate( @@ -250,15 +249,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql` } } } - arrJobs: jobs( - where: { - scheduled_in: { _gte: $start, _lte: $end } - removed: { _eq: false } - } - ) { + arrJobs: jobs(where: { scheduled_in: { _gte: $start, _lte: $end } }) { id scheduled_in - ro_number labhrs: joblines_aggregate( where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } } diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index e910fb3a9..e4c429df8 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -88,6 +88,7 @@ export const QUERY_BODYSHOP = gql` enforce_referral website jc_hourly_rates + md_jobline_presets employees { id active @@ -173,6 +174,7 @@ export const UPDATE_SHOP = gql` enforce_referral website jc_hourly_rates + md_jobline_presets employees { id first_name diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index 1b1304653..90086e27c 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -159,6 +159,7 @@ export const UPDATE_JOB_LINE = gql` db_price act_price line_desc + line_no oem_partno notes location diff --git a/client/src/graphql/vehicles.queries.js b/client/src/graphql/vehicles.queries.js index 55f296e1f..507b22399 100644 --- a/client/src/graphql/vehicles.queries.js +++ b/client/src/graphql/vehicles.queries.js @@ -133,6 +133,36 @@ export const SEARCH_VEHICLE_BY_VIN = gql` } `; +export const SEARCH_VEHICLES = gql` + query SEARCH_VEHICLES($search: String!) { + search_vehicles(args: { search: $search }) { + id + plate_no + plate_st + v_vin + v_model_yr + v_model_desc + v_make_desc + v_color + v_bstyle + updated_at + v_type + v_trimcode + v_tone + v_stage + v_prod_dt + v_paint_codes + v_options + v_mldgcode + v_makecode + v_engine + v_cond + trim_color + db_v_code + } + } +`; + export const SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE = gql` query SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE($id: uuid!) { vehicles_by_pk(id: $id) { diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 63aabf3c0..761b1af85 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -57,7 +57,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { useEffect(() => { document.title = t("titles.jobs-create"); - setSelectedHeader("availablejobs"); + setSelectedHeader("newjob"); setBreadcrumbs([ { link: "/manage/available", label: t("titles.bc.availablejobs") }, { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 1eb0ae3c6..662530f70 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -243,6 +243,7 @@ "street2": "Street 2", "zip": "Zip/Postal Code" }, + "md_jobline_presets": "Jobline Presets", "md_payment_types": "Payment Types", "md_referral_sources": "Referral Sources", "messaginglabel": "Messaging Preset Label", @@ -668,6 +669,7 @@ "addcomponent": "Add Component" }, "errors": { + "refreshrequired": "You must refresh the dashboard data to see this component.", "updatinglayout": "Error saving updated layout {{message}}" }, "labels": { @@ -992,7 +994,8 @@ "billref": "Latest Bill", "edit": "Edit Line", "new": "New Line", - "nostatus": "No Status" + "nostatus": "No Status", + "presets": "Jobline Presets" }, "successes": { "created": "Job line created successfully.", @@ -1440,6 +1443,7 @@ "help": "Help", "home": "Home", "jobs": "Jobs", + "newjob": "Create New Job", "owners": "Owners", "parts-queue": "Parts Queue", "phonebook": "Phonebook", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 6cbc97daa..0c6b45e48 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -243,6 +243,7 @@ "street2": "", "zip": "" }, + "md_jobline_presets": "", "md_payment_types": "", "md_referral_sources": "", "messaginglabel": "", @@ -668,6 +669,7 @@ "addcomponent": "" }, "errors": { + "refreshrequired": "", "updatinglayout": "" }, "labels": { @@ -992,7 +994,8 @@ "billref": "", "edit": "Línea de edición", "new": "Nueva línea", - "nostatus": "" + "nostatus": "", + "presets": "" }, "successes": { "created": "", @@ -1440,6 +1443,7 @@ "help": "", "home": "Casa", "jobs": "Trabajos", + "newjob": "", "owners": "propietarios", "parts-queue": "", "phonebook": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index f9b208bd5..6bce4fe7e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -243,6 +243,7 @@ "street2": "", "zip": "" }, + "md_jobline_presets": "", "md_payment_types": "", "md_referral_sources": "", "messaginglabel": "", @@ -668,6 +669,7 @@ "addcomponent": "" }, "errors": { + "refreshrequired": "", "updatinglayout": "" }, "labels": { @@ -992,7 +994,8 @@ "billref": "", "edit": "Ligne d'édition", "new": "Nouvelle ligne", - "nostatus": "" + "nostatus": "", + "presets": "" }, "successes": { "created": "", @@ -1440,6 +1443,7 @@ "help": "", "home": "Accueil", "jobs": "Emplois", + "newjob": "", "owners": "Propriétaires", "parts-queue": "", "phonebook": "", diff --git a/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/down.yaml b/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/down.yaml new file mode 100644 index 000000000..956e6d1a0 --- /dev/null +++ b/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/down.yaml @@ -0,0 +1,5 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "md_jobline_presets"; + type: run_sql diff --git a/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/up.yaml b/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/up.yaml new file mode 100644 index 000000000..5e1faf8dd --- /dev/null +++ b/hasura/migrations/1623796879914_alter_table_public_bodyshops_add_column_md_jobline_presets/up.yaml @@ -0,0 +1,6 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "md_jobline_presets" jsonb NULL + DEFAULT jsonb_build_array(); + type: run_sql diff --git a/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/down.yaml new file mode 100644 index 000000000..a1885496e --- /dev/null +++ b/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/down.yaml @@ -0,0 +1,84 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_select_permission +- args: + permission: + allow_aggregations: false + columns: + - accountingconfig + - address1 + - address2 + - appt_alt_transport + - appt_colors + - appt_length + - bill_tax_rates + - city + - country + - created_at + - default_adjustment_rate + - deliverchecklist + - email + - enforce_class + - enforce_referral + - federal_tax_id + - id + - imexshopid + - inhousevendorid + - insurance_vendor_id + - intakechecklist + - jc_hourly_rates + - jobsizelimit + - logo_img_path + - md_categories + - md_ccc_rates + - md_classes + - md_hour_split + - md_ins_cos + - md_labor_rates + - md_messaging_presets + - md_notes_presets + - md_order_statuses + - md_parts_locations + - md_payment_types + - md_rbac + - md_referral_sources + - md_responsibility_centers + - md_ro_statuses + - messagingservicesid + - phone + - prodtargethrs + - production_config + - region_config + - schedule_end_time + - schedule_start_time + - scoreboard_target + - shopname + - shoprates + - speedprint + - ssbuckets + - state + - state_tax_id + - stripe_acct_id + - sub_status + - target_touchtime + - template_header + - textid + - updated_at + - use_fippa + - website + - workingdays + - zip_post + computed_fields: [] + filter: + associations: + user: + authid: + _eq: X-Hasura-User-Id + role: user + table: + name: bodyshops + schema: public + type: create_select_permission diff --git a/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/up.yaml new file mode 100644 index 000000000..fae9999b1 --- /dev/null +++ b/hasura/migrations/1623796895613_update_permission_user_public_table_bodyshops/up.yaml @@ -0,0 +1,85 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_select_permission +- args: + permission: + allow_aggregations: false + columns: + - accountingconfig + - address1 + - address2 + - appt_alt_transport + - appt_colors + - appt_length + - bill_tax_rates + - city + - country + - created_at + - default_adjustment_rate + - deliverchecklist + - email + - enforce_class + - enforce_referral + - federal_tax_id + - id + - imexshopid + - inhousevendorid + - insurance_vendor_id + - intakechecklist + - jc_hourly_rates + - jobsizelimit + - logo_img_path + - md_categories + - md_ccc_rates + - md_classes + - md_hour_split + - md_ins_cos + - md_jobline_presets + - md_labor_rates + - md_messaging_presets + - md_notes_presets + - md_order_statuses + - md_parts_locations + - md_payment_types + - md_rbac + - md_referral_sources + - md_responsibility_centers + - md_ro_statuses + - messagingservicesid + - phone + - prodtargethrs + - production_config + - region_config + - schedule_end_time + - schedule_start_time + - scoreboard_target + - shopname + - shoprates + - speedprint + - ssbuckets + - state + - state_tax_id + - stripe_acct_id + - sub_status + - target_touchtime + - template_header + - textid + - updated_at + - use_fippa + - website + - workingdays + - zip_post + computed_fields: [] + filter: + associations: + user: + authid: + _eq: X-Hasura-User-Id + role: user + table: + name: bodyshops + schema: public + type: create_select_permission diff --git a/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/down.yaml new file mode 100644 index 000000000..e8583e4ca --- /dev/null +++ b/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/down.yaml @@ -0,0 +1,78 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_update_permission +- args: + permission: + columns: + - accountingconfig + - address1 + - address2 + - appt_alt_transport + - appt_colors + - appt_length + - bill_tax_rates + - city + - country + - created_at + - default_adjustment_rate + - deliverchecklist + - email + - enforce_class + - enforce_referral + - federal_tax_id + - id + - inhousevendorid + - insurance_vendor_id + - intakechecklist + - jc_hourly_rates + - logo_img_path + - md_categories + - md_ccc_rates + - md_classes + - md_hour_split + - md_ins_cos + - md_labor_rates + - md_messaging_presets + - md_notes_presets + - md_order_statuses + - md_parts_locations + - md_payment_types + - md_rbac + - md_referral_sources + - md_responsibility_centers + - md_ro_statuses + - phone + - prodtargethrs + - production_config + - schedule_end_time + - schedule_start_time + - scoreboard_target + - shopname + - shoprates + - speedprint + - ssbuckets + - state + - state_tax_id + - target_touchtime + - updated_at + - use_fippa + - website + - workingdays + - zip_post + filter: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + set: {} + role: user + table: + name: bodyshops + schema: public + type: create_update_permission diff --git a/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/up.yaml new file mode 100644 index 000000000..b09c84f9e --- /dev/null +++ b/hasura/migrations/1623796904892_update_permission_user_public_table_bodyshops/up.yaml @@ -0,0 +1,79 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_update_permission +- args: + permission: + columns: + - accountingconfig + - address1 + - address2 + - appt_alt_transport + - appt_colors + - appt_length + - bill_tax_rates + - city + - country + - created_at + - default_adjustment_rate + - deliverchecklist + - email + - enforce_class + - enforce_referral + - federal_tax_id + - id + - inhousevendorid + - insurance_vendor_id + - intakechecklist + - jc_hourly_rates + - logo_img_path + - md_categories + - md_ccc_rates + - md_classes + - md_hour_split + - md_ins_cos + - md_jobline_presets + - md_labor_rates + - md_messaging_presets + - md_notes_presets + - md_order_statuses + - md_parts_locations + - md_payment_types + - md_rbac + - md_referral_sources + - md_responsibility_centers + - md_ro_statuses + - phone + - prodtargethrs + - production_config + - schedule_end_time + - schedule_start_time + - scoreboard_target + - shopname + - shoprates + - speedprint + - ssbuckets + - state + - state_tax_id + - target_touchtime + - updated_at + - use_fippa + - website + - workingdays + - zip_post + filter: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true + set: {} + role: user + table: + name: bodyshops + schema: public + type: create_update_permission diff --git a/hasura/migrations/metadata.yaml b/hasura/migrations/metadata.yaml index 2dcbc2190..f9bb3e114 100644 --- a/hasura/migrations/metadata.yaml +++ b/hasura/migrations/metadata.yaml @@ -779,6 +779,7 @@ tables: - md_classes - md_hour_split - md_ins_cos + - md_jobline_presets - md_labor_rates - md_messaging_presets - md_notes_presets @@ -849,6 +850,7 @@ tables: - md_classes - md_hour_split - md_ins_cos + - md_jobline_presets - md_labor_rates - md_messaging_presets - md_notes_presets diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 3d4b70abf..765e787cf 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -31,13 +31,17 @@ exports.default = async (req, res) => { }, }; - console.log("***Number of Failed jobs***: ", erroredJobs.length); + console.log( + "***Number of Failed jobs***: ", + erroredJobs.length, + JSON.stringify(erroredJobs.map((x) => x.error)) + ); var ret = builder .create(autoHouseObject, { version: "1.0", encoding: "UTF-8", }) - .end({ pretty: true }); + .end({ pretty: true, allowEmptyTags: true }); //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml res.type("application/xml"); @@ -48,6 +52,8 @@ exports.default = async (req, res) => { const CreateRepairOrderTag = (job, errorCallback) => { //Level 2 + const repairCosts = CreateCosts(job); + try { const ret = { RepairOrderInformation: { @@ -63,8 +69,8 @@ const CreateRepairOrderTag = (job, errorCallback) => { ShopState: job.bodyshop.state, ShopZip: job.bodyshop.zip_post, ShopPhone: job.bodyshop.phone, - EstimatorID: `${job.est_ct_fn} ${job.est_ct_ln}`, - EstimatorName: `${job.est_ct_fn} ${job.est_ct_ln}`, + EstimatorID: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`, + EstimatorName: `${job.est_ct_fn || ""} ${job.est_ct_ln || ""}`, }, CustomerInformation: { FirstName: job.ownr_fn, @@ -97,7 +103,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { VehiclePaintCode: null, VehicleTrimCode: null, VehicleBodyStyle: null, - DriveableFlag: job.tlos_ind ? "Y" : "N", + DriveableFlag: job.driveable ? "Y" : "N", }, InsuranceInformation: { @@ -251,25 +257,39 @@ const CreateRepairOrderTag = (job, errorCallback) => { }, RevisedTotals: { BodyHours: job.job_totals.rates.lab.hours, + BodyRepairHours: job.joblines + .filter((line) => repairOpCodes.includes(line.lbr_op)) + .reduce((acc, val) => acc + val.mod_lb_hrs, 0), + BodyReplaceHours: job.joblines + .filter((line) => replaceOpCodes.includes(line.lbr_op)) + .reduce((acc, val) => acc + val.mod_lb_hrs, 0), RefinishHours: job.job_totals.rates.lar.hours, MechanicalHours: job.job_totals.rates.lam.hours, StructuralHours: job.job_totals.rates.las.hours, PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat( AHDineroFormat ), - PartsTotalCost: 0, + PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat), PartsOEM: Dinero( job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN.total - ).toFormat(AHDineroFormat), - PartsOEMCost: 0, + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAP && + job.job_totals.parts.parts.list.PAP.total + ) + ) + .toFormat(AHDineroFormat), + PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat), PartsAM: Dinero( job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total ).toFormat(AHDineroFormat), - PartsAMCost: 0, + PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat), PartsReconditioned: null, - PartsReconditionedCost: null, + PartsReconditionedCost: + repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat), PartsRecycled: Dinero( job.job_totals.parts.parts.list.PAR && job.job_totals.parts.parts.list.PAR.total @@ -389,10 +409,108 @@ const CreateRepairOrderTag = (job, errorCallback) => { }; return ret; } catch (error) { + console.log("Error calculating job", error); errorCallback(job, error); } }; +const CreateCosts = (job) => { + //Create a mapping based on AH Requirements + + const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + //console.log("JobCostingPartsTable -> line_val", line_val); + if (!bill_acc[line_val.cost_center]) + bill_acc[line_val.cost_center] = Dinero(); + + bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + + return null; + }); + return bill_acc; + }, {}); + const materialsHours = { mapaHrs: 0, mashHrs: 0 }; + //If the hourly rates for job costing are set, add them in. + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0, + }).multiply(materialsHours.mapaHrs) + ); + } + const ticketTotalsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = Dinero(); + + ticket_acc[ticket_val.cost_center] = ticket_acc[ + ticket_val.cost_center + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply(ticket_val.actualhrs || ticket_val.productivehrs || 0) + ); + + return ticket_acc; + }, + {} + ); + const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs; + + return { + PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + return acc.add(billTotalsByCostCenters[key]); + }, Dinero()), + PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( + billTotalsByCostCenters[defaultCosts.PAP] || Dinero() + ), + PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), + PartsReconditionedCost: Dinero(), + PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAR] || Dinero(), + PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(), + BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), + RefinishLaborTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), + MechanicalLaborTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), + StructuralLaborTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), + PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(), + BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(), + MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(), + StorageTotalCost: Dinero(), + DetailTotal: Dinero(), + DetailTotalCost: Dinero(), + SalesTaxTotalCost: Dinero(), + }; +}; + const StatusMapping = (status, md_ro_statuses) => { //EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED. const { @@ -493,3 +611,6 @@ const generateNullDetailLine = () => { EstimateAmount: null, }; }; + +const repairOpCodes = ["OP4", "OP9", "OP10"]; +const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 6ee17551a..f5f4e837d 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -338,6 +338,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) { rate_mapa rate_mash job_totals + driveable bodyshop { id shopname @@ -350,6 +351,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) { md_ro_statuses md_order_statuses autohouseid + md_responsibility_centers + jc_hourly_rates } joblines (where:{removed: {_eq:false}}){ id @@ -366,7 +369,10 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) { part_qty part_type oem_partno - billlines (order_by:{bill:{date:desc_nulls_last}}) { + lbr_op + profitcenter_part + profitcenter_labor + billlines (order_by:{bill:{date:desc_nulls_last}}) { actual_cost actual_price quantity @@ -377,7 +383,27 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz) { invoice_number } } - } + + } bills { + id + federal_tax_rate + local_tax_rate + state_tax_rate + is_credit_memo + billlines { + actual_cost + cost_center + id + quantity + } + } + timetickets { + id + rate + cost_center + actualhrs + productivehrs + } area_of_damage employee_prep_rel { first_name diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 308c6a806..51f285279 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -353,11 +353,12 @@ function CalculateTaxesTotals(job, otherTotals) { //Audatex sends additional glass part types. IO-774 const BackupGlassTax = - job.parts_tax_rates.PAGD || - job.parts_tax_rates.PAGF || - job.parts_tax_rates.PAGP || - job.parts_tax_rates.PAGQ || - job.parts_tax_rates.PAGR; + job.parts_tax_rates && + (job.parts_tax_rates.PAGD || + job.parts_tax_rates.PAGF || + job.parts_tax_rates.PAGP || + job.parts_tax_rates.PAGQ || + job.parts_tax_rates.PAGR); job.joblines .filter((jl) => !jl.removed)