From 1305277c092c3d589f18933fb1750cf6c05f671a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 9 Jan 2024 11:08:02 -0800 Subject: [PATCH 1/7] IO-2520 Kaizen Data Pump --- server.js | 1 + server/data/claimscorp.js | 2 +- server/data/data.js | 3 +- server/data/kaizen.js | 830 +++++++++++++++++++++++++++++++ server/graphql-client/queries.js | 199 +++++++- 5 files changed, 1032 insertions(+), 3 deletions(-) create mode 100644 server/data/kaizen.js diff --git a/server.js b/server.js index f16d583d5..a64545b72 100644 --- a/server.js +++ b/server.js @@ -224,6 +224,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); app.post("/data/cc", data.claimscorp); +app.post("/data/kaizen", data.kaizen); app.post("/record-handler/arms", data.arms); var taskHandler = require("./server/tasks/tasks"); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 60943aed3..1768f6eb1 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -507,7 +507,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat), Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), - Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), + Frame: repairCosts.FrameLaborTotalCost.toFormat(CCDineroFormat), Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat), Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat), Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat), diff --git a/server/data/data.js b/server/data/data.js index 077f9f134..18ec4c321 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,3 +1,4 @@ +exports.arms = require("./arms").default; exports.autohouse = require("./autohouse").default; exports.claimscorp = require("./claimscorp").default; -exports.arms = require("./arms").default; \ No newline at end of file +exports.kaizen = require("./kaizen").default; \ No newline at end of file diff --git a/server/data/kaizen.js b/server/data/kaizen.js new file mode 100644 index 000000000..57c354e4e --- /dev/null +++ b/server/data/kaizen.js @@ -0,0 +1,830 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const Dinero = require("dinero.js"); +const moment = require("moment-timezone"); +var builder = require("xmlbuilder2"); +const _ = require("lodash"); +const logger = require("../utils/logger"); +const fs = require("fs"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +let Client = require("ssh2-sftp-client"); + +const client = require("../graphql-client/graphql-client").client; +const { sendServerEmail } = require("../email/sendemail"); +const DineroFormat = "0,0.00"; +const DateFormat = "MM/DD/YYYY"; + +const repairOpCodes = ["OP4", "OP9", "OP10"]; +const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; + +const ftpSetup = { + host: process.env.KAIZEN_HOST, + port: process.env.KAIZEN_PORT, + username: process.env.KAIZEN_USER, + password: process.env.KAIZEN_PASSWORD, + debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), + algorithms: { + serverHostKey: ["ssh-rsa", "ssh-dss"], + }, +}; + +exports.default = async (req, res) => { + //Query for the List of Bodyshop Clients. + logger.log("kaizen-start", "DEBUG", "api", null, null); + const kaizenShopsNames = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; + + const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { + shopname: kaizenShopsNames, + }); + + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + res.sendStatus(401); + return; + } + const allxmlsToUpload = []; + const allErrors = []; + try { + for (const bodyshop of specificShopIds + ? bodyshops.filter((b) => specificShopIds.includes(b.id)) + : bodyshops) { + logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + const erroredJobs = []; + try { + const { jobs, bodyshops_by_pk } = await client.request( + queries.KAIZEN_QUERY, + { + bodyshopid: bodyshop.id, + start: start + ? moment(start).startOf("day") + : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }), + } + ); + + const kaizenObject = { + DataFeed: { + ShopInfo: { + ShopName: bodyshops_by_pk.shopname, + Jobs: jobs.map((j) => + CreateRepairOrderTag( + { ...j, bodyshop: bodyshops_by_pk }, + function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + } + ) + ), + }, + }, + }; + + if (erroredJobs.length > 0) { + logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)), + }); + } + + var ret = builder + .create( + { + // version: "1.0", + // encoding: "UTF-8", + //keepNullNodes: true, + }, + kaizenObject + ) + .end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: kaizenObject.DataFeed.ShopInfo.Jobs.length, + xml: ret, + filename: `${bodyshop.shopname}-${moment().format( + "YYYYMMDDTHHMMss" + )}.xml`, + }); + + logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + } catch (error) { + //Error at the shop level. + logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { + ...error, + }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()], + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error, + })), + }); + } + } + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + + res.json(allxmlsToUpload); + 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 + )} + `, + }); + return; + } + + let sftp = new Client(); + sftp.on("error", (errors) => + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...errors, + }) + ); + try { + //Connect to the FTP and upload all. + + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { + filename: xmlObj.filename, + }); + + const uploadResult = await sftp.put( + Buffer.from(xmlObj.xml), + `/${xmlObj.filename}` + ); + logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { + uploadResult, + }); + } + + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + } catch (error) { + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...error, + }); + } finally { + sftp.end(); + } + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + res.sendStatus(200); + } catch (error) { + res.status(200).json(error); + } +}; + +const CreateRepairOrderTag = (job, errorCallback) => { + //Level 2 + + if (!job.job_totals) { + errorCallback({ + jobid: job.id, + job: job, + ro_number: job.ro_number, + error: { toString: () => "No job totals for RO." }, + }); + return {}; + } + + const repairCosts = CreateCosts(job); + + try { + const ret = { + JobID: job.id, + RoNumber: job.ro_number, + JobStatus: job.tlos_ind + ? "Total Loss" + : job.ro_number + ? job.status + : "Estimate", + Customer: { + CompanyName: job.ownr_co_nm?.trim() || "", + FirstName: job.ownr_fn?.trim() || "", + LastName: job.ownr_ln?.trim() || "", + Address1: job.ownr_addr1?.trim() || "", + Address2: job.ownr_addr2?.trim() || "", + City: job.ownr_city?.trim() || "", + State: job.ownr_st?.trim() || "", + Zip: job.ownr_zip?.trim() || "", + }, + Vehicle: { + Year: job.v_model_yr + ? parseInt(job.v_model_yr.match(/\d/g)) + ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) + : "" + : "", + Make: job.v_make_desc || "", + Model: job.v_model_desc || "", + BodyStyle: job.vehicle?.v_bstyle || "", + Color: job.v_color || "", + VIN: job.v_vin || "", + PlateNo: job.plate_no || "", + }, + InsuranceCompany: job.ins_co_nm || "", + Claim: job.clm_no || "", + Contacts: { + CSR: job.employee_csr_rel + ? `${ + job.employee_csr_rel.last_name + ? job.employee_csr_rel.last_name + : "" + }${job.employee_csr_rel.last_name ? ", " : ""}${ + job.employee_csr_rel.first_name + ? job.employee_csr_rel.first_name + : "" + }` + : "", + Estimator: `${job.est_ct_ln ? job.est_ct_ln : ""}${ + job.est_ct_ln ? ", " : "" + }${job.est_ct_fn ? job.est_ct_fn : ""}`, + }, + Dates: { + DateEstimated: + (job.date_estimated && + moment(job.date_estimated).format(DateFormat)) || + "", + DateOpened: + (job.date_opened && moment(job.date_opened).format(DateFormat)) || "", + DateScheduled: + (job.scheduled_in && + moment(job.scheduled_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateArrived: + (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateStart: job.date_repairstarted + ? (job.date_repairstarted && + moment(job.date_repairstarted) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "" + : (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledCompletion: + (job.scheduled_completion && + moment(job.scheduled_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateCompleted: + (job.actual_completion && + moment(job.actual_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledDelivery: + (job.scheduled_delivery && + moment(job.scheduled_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateDelivered: + (job.actual_delivery && + moment(job.actual_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateInvoiced: + (job.date_invoiced && + moment(job.date_invoiced) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateExported: + (job.date_exported && + moment(job.date_exported) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + }, + Sales: { + Labour: { + Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat( + DineroFormat + ), + Body: Dinero(job.job_totals.rates.lab.total).toFormat(DineroFormat), + Diagnostic: Dinero(job.job_totals.rates.lad.total).toFormat( + DineroFormat + ), + Electrical: Dinero(job.job_totals.rates.lae.total).toFormat( + DineroFormat + ), + Frame: Dinero(job.job_totals.rates.laf.total).toFormat(DineroFormat), + Glass: Dinero(job.job_totals.rates.lag.total).toFormat(DineroFormat), + Mechanical: Dinero(job.job_totals.rates.lam.total).toFormat( + DineroFormat + ), + OtherLabour: Dinero(job.job_totals.rates.la1.total) + .add(Dinero(job.job_totals.rates.la2.total)) + .add(Dinero(job.job_totals.rates.la3.total)) + .add(Dinero(job.job_totals.rates.la4.total)) + .add(Dinero(job.job_totals.rates.lau.total)) + .toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.lar.total).toFormat( + DineroFormat + ), + Structural: Dinero(job.job_totals.rates.las.total).toFormat( + DineroFormat + ), + }, + Materials: { + Body: Dinero(job.job_totals.rates.mash.total).toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.mapa.total).toFormat( + DineroFormat + ), + }, + Parts: { + Aftermarket: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ).toFormat(DineroFormat), + LKQ: Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ).toFormat(DineroFormat), + OEM: Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAP && + job.job_totals.parts.parts.list.PAP.total + ) + ) + .toFormat(DineroFormat), + OtherParts: Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ).toFormat(DineroFormat), + Reconditioned: Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ).toFormat(DineroFormat), + TotalParts: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ) + ) + .toFormat(DineroFormat), + }, + OtherSales: Dinero(job.job_totals.additional.storage).toFormat( + DineroFormat + ), + Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat( + DineroFormat + ), + Towing: Dinero(job.job_totals.additional.towing).toFormat(DineroFormat), + ATS: + job.job_totals.additional.additionalCostItems.includes( + "ATS Amount" + ) === true + ? Dinero( + job.job_totals.additional.additionalCostItems[ + job.job_totals.additional.additionalCostItems.indexOf( + "ATS Amount" + ) + ].total + ).toFormat(DineroFormat) + : Dinero().toFormat(DineroFormat), + SaleSubtotal: Dinero(job.job_totals.totals.subtotal).toFormat( + DineroFormat + ), + Tax: Dinero(job.job_totals.totals.local_tax) + .add(Dinero(job.job_totals.totals.state_tax)) + .add(Dinero(job.job_totals.totals.federal_tax)) + .add(Dinero(job.job_totals.additional.pvrt)) + .toFormat(DineroFormat), + SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat( + DineroFormat + ), + }, + SaleHours: { + Aluminum: job.job_totals.rates.laa.hours.toFixed(2), + Body: job.job_totals.rates.lab.hours.toFixed(2), + Diagnostic: job.job_totals.rates.lad.hours.toFixed(2), + Electrical: job.job_totals.rates.lae.hours.toFixed(2), + Frame: job.job_totals.rates.laf.hours.toFixed(2), + Glass: job.job_totals.rates.lag.hours.toFixed(2), + Mechanical: job.job_totals.rates.lam.hours.toFixed(2), + Other: ( + job.job_totals.rates.la1.hours + + job.job_totals.rates.la2.hours + + job.job_totals.rates.la3.hours + + job.job_totals.rates.la4.hours + + job.job_totals.rates.lau.hours + ).toFixed(2), + Refinish: job.job_totals.rates.lar.hours.toFixed(2), + Structural: job.job_totals.rates.las.hours.toFixed(2), + TotalHours: job.joblines + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + .toFixed(2), + }, + Costs: { + Labour: { + Aluminum: repairCosts.AluminumLabourTotalCost.toFormat(DineroFormat), + Body: repairCosts.BodyLabourTotalCost.toFormat(DineroFormat), + Diagnostic: + repairCosts.DiagnosticLabourTotalCost.toFormat(DineroFormat), + Electrical: + repairCosts.ElectricalLabourTotalCost.toFormat(DineroFormat), + Frame: repairCosts.FrameLabourTotalCost.toFormat(DineroFormat), + Glass: repairCosts.GlassLabourTotalCost.toFormat(DineroFormat), + Mechancial: + repairCosts.MechanicalLabourTotalCost.toFormat(DineroFormat), + OtherLabour: repairCosts.LabourMiscTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.RefinishLabourTotalCost.toFormat(DineroFormat), + Structural: + repairCosts.StructuralLabourTotalCost.toFormat(DineroFormat), + TotalLabour: repairCosts.LabourTotalCost.toFormat(DineroFormat), + }, + Materials: { + Body: repairCosts.BMTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.PMTotalCost.toFormat(DineroFormat), + }, + Parts: { + Aftermarket: repairCosts.PartsAMCost.toFormat(DineroFormat), + LKQ: repairCosts.PartsRecycledCost.toFormat(DineroFormat), + OEM: repairCosts.PartsOemCost.toFormat(DineroFormat), + OtherCost: repairCosts.PartsOtherCost.toFormat(DineroFormat), + Reconditioned: + repairCosts.PartsReconditionedCost.toFormat(DineroFormat), + TotalParts: repairCosts.PartsAMCost.add(repairCosts.PartsRecycledCost) + .add(repairCosts.PartsReconditionedCost) + .add(repairCosts.PartsOemCost) + .add(repairCosts.PartsOtherCost) + .toFormat(DineroFormat), + }, + Sublet: repairCosts.SubletTotalCost.toFormat(DineroFormat), + Towing: repairCosts.TowingTotalCost.toFormat(DineroFormat), + ATS: Dinero().toFormat(DineroFormat), + Storage: repairCosts.StorageTotalCost.toFormat(DineroFormat), + CostTotal: repairCosts.TotalCost.toFormat(DineroFormat), + }, + CostHours: { + Aluminum: repairCosts.AluminumLabourTotalHrs.toFixed(2), + Body: repairCosts.BodyLabourTotalHrs.toFixed(2), + Diagnostic: repairCosts.DiagnosticLabourTotalHrs.toFixed(2), + Refinish: repairCosts.RefinishLabourTotalHrs.toFixed(2), + Frame: repairCosts.FrameLabourTotalHrs.toFixed(2), + Mechanical: repairCosts.MechanicalLabourTotalHrs.toFixed(2), + Glass: repairCosts.GlassLabourTotalHrs.toFixed(2), + Electrical: repairCosts.ElectricalLabourTotalHrs.toFixed(2), + Structural: repairCosts.StructuralLabourTotalHrs.toFixed(2), + Other: repairCosts.LabourMiscTotalHrs.toFixed(2), + CostTotalHours: repairCosts.TotalHrs.toFixed(2), + }, + }; + return ret; + } catch (error) { + logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { + error, + }); + + errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); + } +}; + +const CreateCosts = (job) => { + //Create a mapping based on AH Requirements + + //For DMS, the keys in the object below are the CIECA part types. + const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + + 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; + }, {}); + + //If the hourly rates for job costing are set, add them in. + if ( + job.bodyshop.jc_hourly_rates && + (job.bodyshop.jc_hourly_rates.mapa || + typeof job.bodyshop.jc_hourly_rates.mapa === "number" || + isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) + ) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero({ + amount: Math.round( + ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 + ), + }); + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = Dinero(); + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mash * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mash.hours) + ); + } + //Uses CIECA Labour types. + 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.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ) + ); + + return ticket_acc; + }, + {} + ); + const ticketHrsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = 0; + + ticket_acc[ticket_val.cost_center] = + ticket_acc[ticket_val.cost_center] + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0; + + return ticket_acc; + }, + {} + ); + //CIECA STANDARD MAPPING OBJECT. + + const ciecaObj = { + ATS: "ATS", + LA1: "LA1", + LA2: "LA2", + LA3: "LA3", + LA4: "LA4", + LAA: "LAA", + LAB: "LAB", + LAD: "LAD", + LAE: "LAE", + LAF: "LAF", + LAG: "LAG", + LAM: "LAM", + LAR: "LAR", + LAS: "LAS", + LAU: "LAU", + PAA: "PAA", + PAC: "PAC", + PAG: "PAG", + PAL: "PAL", + PAM: "PAM", + PAN: "PAN", + PAO: "PAO", + PAP: "PAP", + PAR: "PAR", + PAS: "PAS", + TOW: "TOW", + MAPA: "MAPA", + MASH: "MASH", + PASL: "PASL", + }; + const defaultCosts = + job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber + ? ciecaObj + : job.bodyshop.md_responsibility_centers.defaults.costs; + + return { + PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + if ( + key !== defaultCosts.PAS && + key !== defaultCosts.PASL && + key !== defaultCosts.MAPA && + key !== defaultCosts.MASH && + key !== defaultCosts.TOW + ) + return acc.add(billTotalsByCostCenters[key]); + return acc; + }, Dinero()), + PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( + billTotalsByCostCenters[defaultCosts.PAP] || Dinero() + ), + PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), + PartsReconditionedCost: + billTotalsByCostCenters[defaultCosts.PAM] || Dinero(), + PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(), + PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + + SubletTotalCost: + billTotalsByCostCenters[defaultCosts.PAS] || + Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()), + + AluminumLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAA] || Dinero(), + AluminumLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAA] || 0, + BodyLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), + BodyLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0, + DiagnosticLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(), + DiagnosticLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAD] || 0, + ElectricalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(), + ElectricalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0, + FrameLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(), + FrameLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0, + GlassLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(), + GlassLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0, + LabourMiscTotalCost: ( + ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero() + ) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()), + LabourMiscTotalHrs: + (ticketHrsByCostCenter[defaultCosts.LA1] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA2] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA3] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA4] || 0) + + (ticketHrsByCostCenter[defaultCosts.LAU] || 0), + MechanicalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), + MechanicalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0, + RefinishLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), + RefinishLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0, + StructuralLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), + StructuralLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0, + + 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(), + LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce( + (acc, key) => { + return acc.add(ticketTotalsByCostCenter[key]); + }, + Dinero() + ), + TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + return acc.add(billTotalsByCostCenters[key]); + }, Dinero()), + TotalHrs: job.timetickets.reduce((acc, ticket_val) => { + return ( + acc + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ); + }, 0), + }; +}; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a27ac8bd0..554388afe 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1070,6 +1070,183 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) { } }`; +exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + address1 + city + state + zip_post + country + phone + last_name_first + md_ro_statuses + md_order_statuses + md_responsibility_centers + jc_hourly_rates + cdk_dealerid + pbs_serialnumber + use_paint_scale_data + timezone + } + jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { + actual_completion + actual_delivery + actual_in + asgn_date + bills { + billlines { + actual_cost + cost_center + id + quantity + } + federal_tax_rate + id + is_credit_memo + local_tax_rate + state_tax_rate + } + created_at + clm_no + date_estimated + date_exported + date_invoiced + date_open + date_repairstarted + employee_body_rel { + first_name + last_name + employee_number + id + } + employee_csr_rel { + first_name + last_name + employee_number + id + } + employee_prep_rel { + first_name + last_name + employee_number + id + } + employee_refinish_rel { + first_name + last_name + employee_number + id + } + est_ct_fn + est_ct_ln + id + ins_co_nm + joblines(where: {removed: {_eq: false}}) { + act_price + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { + actual_cost + actual_price + quantity + bill { + vendor { + name + } + invoice_number + date + } + } + db_price + id + lbr_op + line_desc + line_ind + line_no + mod_lb_hrs + mod_lbr_ty + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + part_qty + part_type + profitcenter_part + profitcenter_labor + prt_dsmk_m + prt_dsmk_p + oem_partno + status + } + job_totals + loss_date + mixdata(limit: 1, order_by: {updated_at: desc}) { + jobid + totalliquidcost + } + ownr_addr1 + ownr_addr2 + ownr_city + ownr_co_nm + ownr_fn + ownr_ln + ownr_st + ownr_zip + parts_orders(limit: 1, order_by: {created_at: desc}) { + created_at + } + parts_tax_rates + plate_no + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_matd + rate_mapa + rate_mash + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + status + timetickets { + id + rate + cost_center + actualhrs + productivehrs + flat_rate + } + tlos_ind + v_color + v_model_yr + v_model_desc + v_make_desc + v_vin + vehicle { + v_bstyle + } + } +}`; + exports.UPDATE_JOB = ` mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) { update_jobs(where: { id: { _eq: $jobId } }, _set: $job) { @@ -1542,7 +1719,7 @@ exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { } }`; -exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { +exports.GET_ENTEGRAL_SHOPS = `query GET_ENTEGRAL_SHOPS { bodyshops(where: {entegral_id: {_is_null: false}, _or: {entegral_id: {_neq: ""}}}){ id shopname @@ -1562,6 +1739,26 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { } }`; +exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($shopname: [String]) { + bodyshops(where: {shopname: {_in: $shopname}}){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + autohouseid + md_responsibility_centers + jc_hourly_rates + imexshopid + timezone + } +}`; + exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{ delete_dms_vehicles(where: {}) { affected_rows From 3e9279d89a9d27ec5ab4b6935937bdbbfbfef2f9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 9 Jan 2024 12:00:24 -0800 Subject: [PATCH 2/7] IO-2520 Change Query Time Bound --- server/data/kaizen.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 57c354e4e..7cd7fef8c 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -64,9 +64,9 @@ exports.default = async (req, res) => { { bodyshopid: bodyshop.id, start: start - ? moment(start).startOf("day") - : moment().subtract(5, "days").startOf("day"), - ...(end && { end: moment(end).endOf("day") }), + ? moment(start).startOf("hours") + : moment().subtract(2, "hours").startOf("hour"), + ...(end && { end: moment(end).endOf("hours") }), } ); From 02b6875eecd5ed98e61a88228cfea9a37bcd942e Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 15:41:10 -0800 Subject: [PATCH 3/7] IO-2602 Beta domain --- server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server.js b/server.js index a64545b72..5eebba671 100644 --- a/server.js +++ b/server.js @@ -34,6 +34,10 @@ const io = new Server(server, { "http://localhost:3000", "https://imex.online", "https://www.imex.online", + "https://beta.test.imex.online", + "https://www.beta.test.imex.online", + "https://beta.imex.online", + "https://www.beta.imex.online", ], methods: ["GET", "POST"], credentials: true, From 04cff4acb15d806877592077a96dc9cd27e053a1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 12 Jan 2024 16:26:09 -0800 Subject: [PATCH 4/7] IO-2520 Add in Server Key format --- server/data/kaizen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/data/kaizen.js b/server/data/kaizen.js index 7cd7fef8c..0df82a9ce 100644 --- a/server/data/kaizen.js +++ b/server/data/kaizen.js @@ -29,7 +29,7 @@ const ftpSetup = { password: process.env.KAIZEN_PASSWORD, debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), algorithms: { - serverHostKey: ["ssh-rsa", "ssh-dss"], + serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"], }, }; From 63ae37e5a99819b88706167a50a6515eaa54e994 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 12 Jan 2024 20:29:35 -0500 Subject: [PATCH 5/7] - small refactor Signed-off-by: Dave Richer --- client/src/components/header/header.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 8bf53c24d..0b099a109 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -42,7 +42,7 @@ import {setModalContext} from "../../redux/modals/modals.actions"; import {signOutStart} from "../../redux/user/user.actions"; import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import {FiLogOut} from "react-icons/fi"; -import handleBeta, {checkBeta, setBeta} from "../../utils/betaHandler"; +import {handleBeta, checkBeta, setBeta} from "../../utils/betaHandler"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, From 7b61c24461f74d52ffe16537a3492055fa6bbdb1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 15 Jan 2024 15:59:00 -0500 Subject: [PATCH 6/7] - Add hover style - Add default theme reference - Revert react-grid-layout to 1.3.4 Signed-off-by: Dave Richer --- client/package-lock.json | 220 ++++++++++++--------------------- client/package.json | 8 +- client/src/App/App.styles.scss | 5 + client/src/index.js | 8 +- 4 files changed, 85 insertions(+), 156 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 5040547ae..8a4397789 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -54,7 +54,7 @@ "react-dom": "^18.2.0", "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.0", - "react-grid-layout": "^1.3.4", + "react-grid-layout": "1.3.4", "react-i18next": "^14.0.0", "react-icons": "^5.0.1", "react-image-lightbox": "^5.1.4", @@ -79,18 +79,12 @@ "subscriptions-transport-ws": "^0.11.0", "terser-webpack-plugin": "^5.3.10", "web-vitals": "^3.5.1", - "workbox-background-sync": "^7.0.0", - "workbox-broadcast-update": "^7.0.0", - "workbox-cacheable-response": "^7.0.0", "workbox-core": "^7.0.0", "workbox-expiration": "^7.0.0", - "workbox-google-analytics": "^7.0.0", "workbox-navigation-preload": "^7.0.0", "workbox-precaching": "^7.0.0", - "workbox-range-requests": "^7.0.0", "workbox-routing": "^7.0.0", "workbox-strategies": "^7.0.0", - "workbox-streams": "^7.0.0", "yauzl": "^2.10.0" }, "devDependencies": { @@ -215,13 +209,13 @@ "integrity": "sha512-4QBZg8ccyC6LPIRii7A0bZUk3+lEDCLnhB+FVsflGdcWPPmV+j3fire4AwwoqHV/BibgvBmR9ZIo4s867smv+g==" }, "node_modules/@ant-design/pro-layout": { - "version": "7.17.16", - "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.17.16.tgz", - "integrity": "sha512-nBQbAJEUkGqQNBqT30FcbcNXHepxySj/O7est1+2iXPXeVeyQYLHBJXcdvCrUrMty6ev3loG8K+6L3DXQkJ/5Q==", + "version": "7.17.19", + "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.17.19.tgz", + "integrity": "sha512-X3L+/0Vro9AyN51oGBMz+bfjHjod43wyMJ+7gePiK8ECqYTMZLWGXhrEZnPMY/GCdk0OeGzWD5N9DFuUtwcSLQ==", "dependencies": { "@ant-design/icons": "^5.0.0", "@ant-design/pro-provider": "2.13.5", - "@ant-design/pro-utils": "2.15.2", + "@ant-design/pro-utils": "2.15.4", "@babel/runtime": "^7.18.0", "@umijs/route-utils": "^4.0.0", "@umijs/use-params": "^1.0.9", @@ -258,9 +252,9 @@ } }, "node_modules/@ant-design/pro-utils": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.15.2.tgz", - "integrity": "sha512-kzMT658CGrQqoihKPlZhbnRioovwMhM59vqqVXuT6A+IuyxJhs5pxN73C6sN2+ZoeYCo/Gewnfn9v1/2vHU2Zg==", + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.15.4.tgz", + "integrity": "sha512-nbacIMl5lbMlNHlaPl2tt/ezvHhnBtGL2KLVaqijLou5zAuZprkHAJnckXoqm9T6X9R2rE4jH96WZHLpJ27nFw==", "dependencies": { "@ant-design/icons": "^5.0.0", "@ant-design/pro-provider": "2.13.5", @@ -4281,9 +4275,9 @@ } }, "node_modules/@rc-component/tour": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.11.1.tgz", - "integrity": "sha512-c9Lw3/oVinj5D64Rsp8aDLOXcgdViE+hq7bj0Qoo8fTuQEh9sSpUw5OZcum943JkjeIE4hLcc5FD4a5ANtMJ4w==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.12.1.tgz", + "integrity": "sha512-P2pvSN+rImacOoTuT4NZ+tdjJmxoi6FZ9w++7GnYK6hVeU8qD+iLnDHkgtE7tuvpZxIRS3dPF5O2ykupHx9E/g==", "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", @@ -6553,9 +6547,9 @@ } }, "node_modules/antd": { - "version": "5.12.8", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.12.8.tgz", - "integrity": "sha512-R2CRcB+aaVZurb3J0IKpBRWq5kW4CLcSqDF58/QBsqYdzK7XjSvM8+eF3rWVRUDbSJfGmyW7I80ywNRYpW1+vA==", + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.13.1.tgz", + "integrity": "sha512-/qAPsr6UyJPSFZQD9G7kW98GelH2Bajli+1q7CRW4IinYQ0R0UVJckFX11emByhiU4Jd4WNH/hOO+fZtp0eVDA==", "dependencies": { "@ant-design/colors": "^7.0.2", "@ant-design/cssinjs": "^1.18.2", @@ -6564,23 +6558,23 @@ "@ctrl/tinycolor": "^3.6.1", "@rc-component/color-picker": "~1.5.1", "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/tour": "~1.11.1", + "@rc-component/tour": "~1.12.1", "@rc-component/trigger": "^1.18.2", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "qrcode.react": "^3.1.0", - "rc-cascader": "~3.20.0", + "rc-cascader": "~3.21.0", "rc-checkbox": "~3.1.0", "rc-collapse": "~3.7.2", "rc-dialog": "~9.3.4", - "rc-drawer": "~6.5.2", + "rc-drawer": "~7.0.0", "rc-dropdown": "~4.1.0", "rc-field-form": "~1.41.0", "rc-image": "~7.5.1", - "rc-input": "~1.3.11", - "rc-input-number": "~8.4.0", - "rc-mentions": "~2.9.1", + "rc-input": "~1.4.3", + "rc-input-number": "~8.6.1", + "rc-mentions": "~2.10.1", "rc-menu": "~9.12.4", "rc-motion": "^2.9.0", "rc-notification": "~5.3.0", @@ -6590,16 +6584,16 @@ "rc-rate": "~2.12.0", "rc-resize-observer": "^1.4.0", "rc-segmented": "~2.2.2", - "rc-select": "~14.10.0", + "rc-select": "~14.11.0", "rc-slider": "~10.5.0", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.36.1", - "rc-tabs": "~12.14.1", - "rc-textarea": "~1.5.3", + "rc-table": "~7.37.0", + "rc-tabs": "~14.0.0", + "rc-textarea": "~1.6.3", "rc-tooltip": "~6.1.3", "rc-tree": "~5.8.2", - "rc-tree-select": "~5.15.0", + "rc-tree-select": "~5.17.0", "rc-upload": "~4.5.2", "rc-util": "^5.38.1", "scroll-into-view-if-needed": "^3.1.0", @@ -10766,11 +10760,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==" - }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -14538,6 +14527,11 @@ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14746,9 +14740,9 @@ } }, "node_modules/markerjs2": { - "version": "2.31.4", - "resolved": "https://registry.npmjs.org/markerjs2/-/markerjs2-2.31.4.tgz", - "integrity": "sha512-Xx/2969Uj1VdwFRMcvHlhMNqvFMwJwie8J6WQcj9NDEByD2CdLVxF/39w0rg4ILJbsafAVp0ZxywXiYG9wfI0Q==" + "version": "2.31.5", + "resolved": "https://registry.npmjs.org/markerjs2/-/markerjs2-2.31.5.tgz", + "integrity": "sha512-fOk0IPfqmwdmKlcaf9qfD8SnBBEzJtgl6NVcDOQpUJAcyyk6/wkA4m410NQ8mD7B42WdjkAzAGj2BMxHsSYthQ==" }, "node_modules/material-colors": { "version": "1.2.6", @@ -17385,14 +17379,14 @@ } }, "node_modules/rc-cascader": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.20.0.tgz", - "integrity": "sha512-lkT9EEwOcYdjZ/jvhLoXGzprK1sijT3/Tp4BLxQQcHDZkkOzzwYQC9HgmKoJz0K7CukMfgvO9KqHeBdgE+pELw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.0.tgz", + "integrity": "sha512-7aADjbfqiR4HrTHG9S019p2jeKM/AxISPA5+sBJR7Mlhm/i+lR7VjBju3KQulJNJLKNEnQYg4TFhcPf2SLua9g==", "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", "classnames": "^2.3.1", - "rc-select": "~14.10.0", + "rc-select": "~14.11.0-0", "rc-tree": "~5.8.1", "rc-util": "^5.37.0" }, @@ -17447,9 +17441,9 @@ } }, "node_modules/rc-drawer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-6.5.2.tgz", - "integrity": "sha512-QckxAnQNdhh4vtmKN0ZwDf3iakO83W9eZcSKWYYTDv4qcD2fHhRAZJJ/OE6v2ZlQ2kSqCJX5gYssF4HJFvsEPQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.0.0.tgz", + "integrity": "sha512-ePcS4KtQnn57bCbVXazHN2iC8nTPCXlWEIA/Pft87Pd9U7ZeDkdRzG47jWG2/TAFXFlFltRAMcslqmUM8NPCGA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.1.1", @@ -17548,9 +17542,9 @@ } }, "node_modules/rc-input": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.3.11.tgz", - "integrity": "sha512-jhH7QP5rILanSHCGSUkdoFE5DEtpv8FIseYhuYkOZzUBeiVAiwM3q26YqZ6xBB0QFEZ/yUAgms4xW4iuub3xFQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.3.tgz", + "integrity": "sha512-aHyQUAIRmTlOnvk5EcNqEpJ+XMtfMpYRAJayIlJfsvvH9cAKUWboh4egm23vgMA7E+c/qm4BZcnrDcA960GC1w==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -17562,14 +17556,14 @@ } }, "node_modules/rc-input-number": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-8.4.0.tgz", - "integrity": "sha512-B6rziPOLRmeP7kcS5qbdC5hXvvDHYKV4vUxmahevYx2E6crS2bRi0xLDjhJ0E1HtOWo8rTmaE2EBJAkTCZOLdA==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-8.6.1.tgz", + "integrity": "sha512-gaAMUKtUKLktJ3Yx93tjgYY1M0HunnoqzPEqkb9//Ydup4DcG0TFL9yHBA3pgVdNIt5f0UWyHCgFBj//JxeD6A==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", - "rc-input": "~1.3.5", + "rc-input": "~1.4.0", "rc-util": "^5.28.0" }, "peerDependencies": { @@ -17578,16 +17572,16 @@ } }, "node_modules/rc-mentions": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.9.1.tgz", - "integrity": "sha512-cZuElWr/5Ws0PXx1uxobxfYh4mqUw2FitfabR62YnWgm+WAfDyXZXqZg5DxXW+M1cgVvntrQgDDd9LrihrXzew==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.10.1.tgz", + "integrity": "sha512-72qsEcr/7su+a07ndJ1j8rI9n0Ka/ngWOLYnWMMv0p2mi/5zPwPrEDTt6Uqpe8FWjWhueDJx/vzunL6IdKDYMg==", "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^1.5.0", "classnames": "^2.2.6", - "rc-input": "~1.3.5", + "rc-input": "~1.4.0", "rc-menu": "~9.12.0", - "rc-textarea": "~1.5.0", + "rc-textarea": "~1.6.1", "rc-util": "^5.34.1" }, "peerDependencies": { @@ -17797,9 +17791,9 @@ } }, "node_modules/rc-select": { - "version": "14.10.0", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.10.0.tgz", - "integrity": "sha512-TsIJTYafTTapCA32LLNpx/AD6ntepR1TG8jEVx35NiAAWCPymhUfuca8kRcUNd3WIGVMDcMKn9kkphoxEz+6Ag==", + "version": "14.11.0", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.11.0.tgz", + "integrity": "sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^1.5.0", @@ -17866,9 +17860,9 @@ } }, "node_modules/rc-table": { - "version": "7.36.1", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.36.1.tgz", - "integrity": "sha512-9qMxEm/3Y8ukdW8I8ZvmhX0QImfNKzH0JEUlSbyaUlsYTB+/tQEbfaB8YkG4sHVZ1io4pxqK/BXoZYqebi/TIQ==", + "version": "7.37.0", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.37.0.tgz", + "integrity": "sha512-hEB17ktLRVfVmdo+U8MjGr+PuIgdQ8Cxj/N5lwMvP/Az7TOrQxwTMLVEDoj207tyPYLTWifHIF9EJREWwyk67g==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", @@ -17886,9 +17880,9 @@ } }, "node_modules/rc-tabs": { - "version": "12.14.1", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-12.14.1.tgz", - "integrity": "sha512-1xlE7JQNYxD5RwBsM7jf2xSdUrkmTSDFLFEm2gqAgnsRlOGydEzXXNAVTOT6QcgM1G/gCm+AgG+FYPUGb4Hs4g==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.0.0.tgz", + "integrity": "sha512-lp1YWkaPnjlyhOZCPrAWxK6/P6nMGX/BAZcAC3nuVwKz0Byfp+vNnQKK8BRCP2g/fzu+SeB5dm9aUigRu3tRkQ==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", @@ -17907,13 +17901,13 @@ } }, "node_modules/rc-textarea": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.5.3.tgz", - "integrity": "sha512-oH682ghHx++stFNYrosPRBfwsypywrTXpaD0/5Z8MPkUOnyOQUaY9ueL9tMu6BP1LfsuYQ1VLpg5OtshViLNgA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.6.3.tgz", + "integrity": "sha512-8k7+8Y2GJ/cQLiClFMg8kUXOOdvcFQrnGeSchOvI2ZMIVvX5a3zQpLxoODL0HTrvU63fPkRmMuqaEcOF9dQemA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", - "rc-input": "~1.3.5", + "rc-input": "~1.4.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, @@ -17956,13 +17950,13 @@ } }, "node_modules/rc-tree-select": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.15.0.tgz", - "integrity": "sha512-YJHfdO6azFnR0/JuNBZLDptGE4/RGfVeHAafUIYcm2T3RBkL1O8aVqiHvwIyLzdK59ry0NLrByd+3TkfpRM+9Q==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.17.0.tgz", + "integrity": "sha512-7sRGafswBhf7n6IuHyCEFCildwQIgyKiV8zfYyUoWfZEFdhuk7lCH+DN0aHt+oJrdiY9+6Io/LDXloGe01O8XQ==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.10.0", + "rc-select": "~14.11.0-0", "rc-tree": "~5.8.1", "rc-util": "^5.16.1" }, @@ -18294,30 +18288,21 @@ } }, "node_modules/react-grid-layout": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.4.4.tgz", - "integrity": "sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.3.4.tgz", + "integrity": "sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==", "dependencies": { - "clsx": "^2.0.0", - "fast-equals": "^4.0.3", + "clsx": "^1.1.1", + "lodash.isequal": "^4.0.0", "prop-types": "^15.8.1", - "react-draggable": "^4.4.5", - "react-resizable": "^3.0.5", - "resize-observer-polyfill": "^1.5.1" + "react-draggable": "^4.0.0", + "react-resizable": "^3.0.4" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, - "node_modules/react-grid-layout/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/react-i18next": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.0.tgz", @@ -22507,23 +22492,6 @@ "node": ">=0.10.0" } }, - "node_modules/workbox-background-sync": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz", - "integrity": "sha512-S+m1+84gjdueM+jIKZ+I0Lx0BDHkk5Nu6a3kTVxP4fdj3gKouRNmhO8H290ybnJTOPfBDtTMXSQA/QLTvr7PeA==", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "7.0.0" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.0.0.tgz", - "integrity": "sha512-oUuh4jzZrLySOo0tC0WoKiSg90bVAcnE98uW7F8GFiSOXnhogfNDGZelPJa+6KpGBO5+Qelv04Hqx2UD+BJqNQ==", - "dependencies": { - "workbox-core": "7.0.0" - } - }, "node_modules/workbox-build": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", @@ -22766,14 +22734,6 @@ "workbox-routing": "6.6.0" } }, - "node_modules/workbox-cacheable-response": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.0.0.tgz", - "integrity": "sha512-0lrtyGHn/LH8kKAJVOQfSu3/80WDc9Ma8ng0p2i/5HuUndGttH+mGMSvOskjOdFImLs2XZIimErp7tSOPmu/6g==", - "dependencies": { - "workbox-core": "7.0.0" - } - }, "node_modules/workbox-core": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.0.0.tgz", @@ -22788,17 +22748,6 @@ "workbox-core": "7.0.0" } }, - "node_modules/workbox-google-analytics": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.0.0.tgz", - "integrity": "sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==", - "dependencies": { - "workbox-background-sync": "7.0.0", - "workbox-core": "7.0.0", - "workbox-routing": "7.0.0", - "workbox-strategies": "7.0.0" - } - }, "node_modules/workbox-navigation-preload": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.0.0.tgz", @@ -22817,14 +22766,6 @@ "workbox-strategies": "7.0.0" } }, - "node_modules/workbox-range-requests": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.0.0.tgz", - "integrity": "sha512-SxAzoVl9j/zRU9OT5+IQs7pbJBOUOlriB8Gn9YMvi38BNZRbM+RvkujHMo8FOe9IWrqqwYgDFBfv6sk76I1yaQ==", - "dependencies": { - "workbox-core": "7.0.0" - } - }, "node_modules/workbox-recipes": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", @@ -22903,15 +22844,6 @@ "workbox-core": "7.0.0" } }, - "node_modules/workbox-streams": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.0.0.tgz", - "integrity": "sha512-moVsh+5to//l6IERWceYKGiftc+prNnqOp2sgALJJFbnNVpTXzKISlTIsrWY+ogMqt+x1oMazIdHj25kBSq/HQ==", - "dependencies": { - "workbox-core": "7.0.0", - "workbox-routing": "7.0.0" - } - }, "node_modules/workbox-sw": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", diff --git a/client/package.json b/client/package.json index f50df71ea..f8d1b1c2e 100644 --- a/client/package.json +++ b/client/package.json @@ -50,7 +50,7 @@ "react-dom": "^18.2.0", "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.0", - "react-grid-layout": "^1.3.4", + "react-grid-layout": "1.3.4", "react-i18next": "^14.0.0", "react-icons": "^5.0.1", "react-image-lightbox": "^5.1.4", @@ -75,18 +75,12 @@ "subscriptions-transport-ws": "^0.11.0", "terser-webpack-plugin": "^5.3.10", "web-vitals": "^3.5.1", - "workbox-background-sync": "^7.0.0", - "workbox-broadcast-update": "^7.0.0", - "workbox-cacheable-response": "^7.0.0", "workbox-core": "^7.0.0", "workbox-expiration": "^7.0.0", - "workbox-google-analytics": "^7.0.0", "workbox-navigation-preload": "^7.0.0", "workbox-precaching": "^7.0.0", - "workbox-range-requests": "^7.0.0", "workbox-routing": "^7.0.0", "workbox-strategies": "^7.0.0", - "workbox-streams": "^7.0.0", "yauzl": "^2.10.0" }, "scripts": { diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 736093151..63358885a 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -1,9 +1,14 @@ //Global Styles. @import "react-big-calendar/lib/sass/styles"; + .ant-menu-item-divider { border-bottom: 1px solid #74695c !important; } +.ant-menu-dark .ant-menu-item:hover { + background-color: #1890ff !important; +} + .imex-table-header { display: flex; flex-wrap: wrap; diff --git a/client/src/index.js b/client/src/index.js index ff900b727..9bf4faddb 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -13,6 +13,7 @@ import reportWebVitals from "./reportWebVitals"; import "./translations/i18n"; import "./utils/CleanAxios"; import {ConfigProvider} from "antd"; +import {defaultTheme} from "@ant-design/compatible"; //import { BrowserTracing } from "@sentry/tracing"; //import "antd/dist/antd.css"; // import "antd/dist/antd.less"; @@ -64,12 +65,9 @@ function App() { // Used for ANTD Component Tokens // https://ant.design/docs/react/migrate-less-variables -const themeConfig = { -}; - - +// TODO - Client Update - At a later time it might be worth removing these defaults ReactDOM.createRoot(document.getElementById('root')).render( - + ); From cf9b03d0735c2d924795d8b8d0b5cd8dd449915b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 15 Jan 2024 17:28:24 -0500 Subject: [PATCH 7/7] - Chat formatting - Scroll to Top Button Signed-off-by: Dave Richer --- .../chat-conversation-list.component.jsx | 98 +++++++++---------- .../chat-conversation-list.styles.scss | 15 +-- .../pages/manage/manage.page.component.jsx | 2 +- 3 files changed, 52 insertions(+), 63 deletions(-) diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index 0642884c1..1f9f66e7b 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,4 +1,4 @@ -import { Badge, List, Tag } from "antd"; +import {Badge, List, Space, Tag} from "antd"; import React from "react"; import { connect } from "react-redux"; import { @@ -38,57 +38,57 @@ function ChatConversationListComponent({ const rowRenderer = ({ index, key, style, parent }) => { const item = conversationList[index]; - return ( - - setSelectedConversation(item.id)} - className={`chat-list-item ${ - item.id === selectedConversation - ? "chat-list-selected-conversation" - : null - }`} - style={style} + -
setSelectedConversation(item.id)} + style={style} + className={`chat-list-item + ${ + item.id === selectedConversation + ? "chat-list-selected-conversation" + : null + }`} > - {item.label &&
{item.label}
} - {item.job_conversations.length > 0 ? ( -
- {item.job_conversations.map((j, idx) => ( -
- -
- ))} -
- ) : ( - {item.phone_num} - )} -
-
-
- {item.job_conversations.length > 0 - ? item.job_conversations.map((j, idx) => ( - - {j.job.ro_number} - - )) - : null} -
- {item.updated_at} -
- -
-
+ + + {item.label && {item.label}} + {item.job_conversations.length > 0 ? ( + + {item.job_conversations.map((j, idx) => ( + + ))} + + ) : ( + + {item.phone_num} + + )} + + + + {item.job_conversations.length > 0 + ? item.job_conversations.map((j, idx) => ( + {j.job.ro_number} + )) + : null} + + + {item.updated_at} + + + + + + + + ); }; diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss index 20cf8f4ef..44f6f8ec8 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss +++ b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss @@ -2,26 +2,15 @@ background-color: rgba(128, 128, 128, 0.2); } .chat-list-container { - flex: 1; overflow: hidden; height: 100%; border: 1px solid gainsboro; } - .chat-list-item { - display: flex; - flex-direction: row; &:hover { cursor: pointer; color: #ff7a00; } - .chat-name { - flex: 1; - display: inline; - } - .ro-number-tag { - align-self: baseline; - } - padding: 12px 24px; + border-bottom: 1px solid gainsboro; -} +} \ No newline at end of file diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index f3c71bed2..58658b39b 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -373,7 +373,7 @@ export function Manage({conflict, bodyshop}) { {PageContent} - +