Merge branch 'release/2024-12-06' into feature/IO-3020-IO-3036-imex-lite-rome-lite

This commit is contained in:
Patrick Fic
2024-12-06 11:00:00 -08:00
16 changed files with 1067 additions and 109 deletions

View File

@@ -0,0 +1,5 @@
#!/bin/bash
DD_API_KEY=58d91898a70c6fd659f6eea768a57976 DD_SITE="us3.datadoghq.com" bash -c "$(curl -L https://install.datadoghq.com/scripts/install_script_agent7.sh)"
echo "Datadog agent installed."

View File

@@ -7,7 +7,6 @@ RUN dnf install -y git \
&& dnf install -y nodejs \ && dnf install -y nodejs \
&& dnf clean all && dnf clean all
# Install dependencies required by node-canvas # Install dependencies required by node-canvas
RUN dnf install -y \ RUN dnf install -y \
gcc \ gcc \
@@ -19,9 +18,22 @@ RUN dnf install -y \
libpng-devel \ libpng-devel \
make \ make \
python3 \ python3 \
fontconfig \
freetype \
python3-pip \ python3-pip \
wget \
unzip \
&& dnf clean all && dnf clean all
# Install Montserrat fonts
RUN cd /tmp \
&& wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip \
&& unzip montserrat.zip -d montserrat \
&& mv montserrat/montserrat/*.ttf /usr/share/fonts \
&& fc-cache -fv \
&& rm -rf /tmp/montserrat /tmp/montserrat.zip \
&& echo "Montserrat fonts installed and cached successfully."
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app

View File

@@ -25,6 +25,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Form.Item label={t("owners.fields.ownr_co_nm")} name="ownr_co_nm"> <Form.Item label={t("owners.fields.ownr_co_nm")} name="ownr_co_nm">
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
<Input disabled/>
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}> <LayoutFormRow header={t("owners.forms.address")}>
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1"> <Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">

View File

@@ -48,6 +48,7 @@ export const QUERY_OWNER_BY_ID = gql`
query QUERY_OWNER_BY_ID($id: uuid!) { query QUERY_OWNER_BY_ID($id: uuid!) {
owners_by_pk(id: $id) { owners_by_pk(id: $id) {
id id
accountingid
allow_text_message allow_text_message
ownr_addr1 ownr_addr1
ownr_addr2 ownr_addr2

View File

@@ -2397,6 +2397,7 @@
"selectexistingornew": "Select an existing owner record or create a new one. " "selectexistingornew": "Select an existing owner record or create a new one. "
}, },
"fields": { "fields": {
"accountingid": "Accounting ID",
"address": "Address", "address": "Address",
"allow_text_message": "Permission to Text?", "allow_text_message": "Permission to Text?",
"name": "Name", "name": "Name",
@@ -3060,6 +3061,7 @@
"production_not_production_status": "Production not in Production Status", "production_not_production_status": "Production not in Production Status",
"production_over_time": "Production Level over Time", "production_over_time": "Production Level over Time",
"psr_by_make": "Percent of Sales by Vehicle Make", "psr_by_make": "Percent of Sales by Vehicle Make",
"purchase_return_ratio_excel": "Purchase & Return Ratio - Excel",
"purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)", "purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)",
"purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)", "purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)",
"purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)", "purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)",
@@ -3085,6 +3087,7 @@
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
"timetickets_employee": "Employee Time Tickets", "timetickets_employee": "Employee Time Tickets",
"timetickets_summary": "Time Tickets Summary", "timetickets_summary": "Time Tickets Summary",
"total_loss_jobs": "Jobs Marked as Total Loss",
"unclaimed_hrs": "Unflagged Hours", "unclaimed_hrs": "Unflagged Hours",
"void_ros": "Void ROs", "void_ros": "Void ROs",
"work_in_progress_committed_labour": "Work in Progress - Committed Labor", "work_in_progress_committed_labour": "Work in Progress - Committed Labor",

View File

@@ -2397,6 +2397,7 @@
"selectexistingornew": "" "selectexistingornew": ""
}, },
"fields": { "fields": {
"accountingid": "",
"address": "Dirección", "address": "Dirección",
"allow_text_message": "Permiso de texto?", "allow_text_message": "Permiso de texto?",
"name": "Nombre", "name": "Nombre",
@@ -3060,6 +3061,7 @@
"production_not_production_status": "", "production_not_production_status": "",
"production_over_time": "", "production_over_time": "",
"psr_by_make": "", "psr_by_make": "",
"purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "", "purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "", "purchases_by_cost_center_detail": "",
@@ -3085,6 +3087,7 @@
"timetickets": "", "timetickets": "",
"timetickets_employee": "", "timetickets_employee": "",
"timetickets_summary": "", "timetickets_summary": "",
"total_loss_jobs": "",
"unclaimed_hrs": "", "unclaimed_hrs": "",
"void_ros": "", "void_ros": "",
"work_in_progress_committed_labour": "", "work_in_progress_committed_labour": "",

View File

@@ -2397,6 +2397,7 @@
"selectexistingornew": "" "selectexistingornew": ""
}, },
"fields": { "fields": {
"accountingid": "",
"address": "Adresse", "address": "Adresse",
"allow_text_message": "Autorisation de texte?", "allow_text_message": "Autorisation de texte?",
"name": "Prénom", "name": "Prénom",
@@ -3060,6 +3061,7 @@
"production_not_production_status": "", "production_not_production_status": "",
"production_over_time": "", "production_over_time": "",
"psr_by_make": "", "psr_by_make": "",
"purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "", "purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "", "purchases_by_cost_center_detail": "",
@@ -3085,6 +3087,7 @@
"timetickets": "", "timetickets": "",
"timetickets_employee": "", "timetickets_employee": "",
"timetickets_summary": "", "timetickets_summary": "",
"total_loss_jobs": "",
"unclaimed_hrs": "", "unclaimed_hrs": "",
"void_ros": "", "void_ros": "",
"work_in_progress_committed_labour": "", "work_in_progress_committed_labour": "",

View File

@@ -2182,6 +2182,30 @@ export const TemplateList = (type, context) => {
}, },
group: "payroll", group: "payroll",
adp_payroll: true adp_payroll: true
},
purchase_return_ratio_excel: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
subject: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
key: "purchase_return_ratio_excel",
//idtype: "vendor",
reporttype: "excel",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.bills"),
field: i18n.t("bills.fields.date")
},
group: "purchases"
},
total_loss_jobs: {
title: i18n.t("reportcenter.templates.total_loss_jobs"),
subject: i18n.t("reportcenter.templates.total_loss_jobs"),
key: "total_loss_jobs",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open")
},
group: "jobs"
} }
} }
: {}), : {}),

868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,6 +41,7 @@
"cors": "2.8.5", "cors": "2.8.5",
"crisp-status-reporter": "^1.2.2", "crisp-status-reporter": "^1.2.2",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dd-trace": "^5.28.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
@@ -51,6 +52,7 @@
"intuit-oauth": "^4.1.3", "intuit-oauth": "^4.1.3",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"json-2-csv": "^5.5.6", "json-2-csv": "^5.5.6",
"juice": "^11.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.46",
@@ -62,6 +64,7 @@
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"redis": "^4.7.0", "redis": "^4.7.0",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6", "soap": "^1.1.6",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5", "socket.io-adapter": "^2.5.5",

View File

@@ -4,6 +4,14 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
}); });
if (process.env.NODE_ENV) {
const tracer = require("dd-trace").init({
profiling: true,
env: process.env.NODE_ENV,
service: "bodyshop-api"
});
}
const cors = require("cors"); const cors = require("cors");
const http = require("http"); const http = require("http");
const Redis = require("ioredis"); const Redis = require("ioredis");

View File

@@ -328,6 +328,7 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
PostalCode: job.ownr_zip, PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st CountrySubDivisionCode: job.ownr_st
}, },
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
...(isThreeTier ...(isThreeTier
? { ? {
Job: true, Job: true,
@@ -395,7 +396,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
PostalCode: job.ownr_zip, PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st CountrySubDivisionCode: job.ownr_st
}, },
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
Job: true, Job: true,
ParentRef: { ParentRef: {
value: parentTierRef.Id value: parentTierRef.Id
@@ -556,7 +557,8 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(), Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "", Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}` Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
} },
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
}; };
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
@@ -673,7 +675,8 @@ async function InsertInvoiceMultiPayerInvoice(
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(), Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "", Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}` Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
} },
...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
}; };
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, { logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {

View File

@@ -0,0 +1,32 @@
const { isObject } = require("lodash");
const validateCanvasInputMiddleware = (req, res, next) => {
const { values, keys, override, w, h } = req.body;
if (!Array.isArray(values) || !Array.isArray(keys)) {
return res.status(400).send("Invalid input: 'values' and 'keys' must be arrays.");
}
if (values.some((value) => typeof value !== "number")) {
return res.status(400).send("Invalid input: 'values' must be an array of numbers.");
}
if (keys.some((key) => typeof key !== "string")) {
return res.status(400).send("Invalid input: 'keys' must be an array of strings.");
}
if (override && !isObject(override)) {
return res.status(400).send("Override must be an object");
}
if (w && (!Number.isFinite(w) || w <= 0)) {
return res.status(400).send("Width must be a positive number");
}
if (h && (!Number.isFinite(h) || h <= 0)) {
return res.status(400).send("Height must be a positive number");
}
next(); // Proceed to the next middleware or route handler
};
module.exports = validateCanvasInputMiddleware;

View File

@@ -1,43 +1,31 @@
const { createCanvas } = require("canvas"); const { createCanvas } = require("canvas");
const { Canvas, FontLibrary } = require("skia-canvas");
const Chart = require("chart.js/auto"); const Chart = require("chart.js/auto");
const logger = require("../utils/logger");
const { backgroundColors, borderColors } = require("./canvas-colors"); const { backgroundColors, borderColors } = require("./canvas-colors");
const { isObject, defaultsDeep, isNumber } = require("lodash"); const { defaultsDeep, isNumber } = require("lodash");
exports.canvastest = function (req, res) { const CANVAS_QUEUE_LIMIT = 100;
//console.log("Incoming test request.", req);
res.status(200).send("OK");
};
exports.canvas = function (req, res) { let isProcessing = false;
const { w, h, values, keys, override } = req.body; const requestQueue = [];
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Gate required values
if (!values || !keys) {
res.status(400).send("Missing required data");
return;
}
// Override must be an object if it exists try {
if (override && !isObject(override)) { FontLibrary.use("Montserrat", [
res.status(400).send("Override must be an object"); "/usr/share/fonts/Montserrat-Regular.ttf",
return; "/usr/share/fonts/Montserrat-Bold.ttf",
} "/usr/share/fonts/Montserrat-Italic.ttf"
]);
} catch (error) {
console.error(
"Error loading fonts Skia Canvas Fonts, please be sure to install Montserrat font package",
error.message
);
}
// Set the default Width and Height // Utility to create a chart configuration
let [width, height] = [500, 275]; const getChartConfiguration = (keys, values, override) => {
const defaultConfiguration = {
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = {
type: "doughnut", type: "doughnut",
data: { data: {
labels: keys, labels: keys,
@@ -53,6 +41,7 @@ exports.canvas = function (req, res) {
options: { options: {
devicePixelRatio: 4, devicePixelRatio: 4,
responsive: false, responsive: false,
animation: false,
maintainAspectRatio: true, maintainAspectRatio: true,
circumference: 180, circumference: 180,
rotation: -90, rotation: -90,
@@ -73,21 +62,88 @@ exports.canvas = function (req, res) {
} }
}; };
// If we have a valid override object, merge it with the default configuration object. return defaultsDeep(override || {}, defaultConfiguration);
// This allows for you to override the default configuration with a custom one. };
const defaults = () => {
if (!override || !isObject(override)) { const processCanvasRequest = async (req, res, isSkia = false) => {
return configuration; const { logger } = req;
} const { w, h, values, keys, override } = req.body;
return defaultsDeep(override, configuration);
}; logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
res.status(200).send( // Default width and height
(() => { const width = isNumber(w) && w > 0 ? w : 500;
const canvas = createCanvas(width, height); const height = isNumber(h) && h > 0 ? h : 275;
const ctx = canvas.getContext("2d");
new Chart(ctx, defaults()); const configuration = getChartConfiguration(keys, values, override);
return canvas.toDataURL();
})() // Placeholders to allow fine control over GAC
); let canvas = null;
let ctx = null;
let chart = null;
let chartImage = null;
try {
// Create the canvas
canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height);
ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL();
res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
} catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
res.status(500).send("Failed to generate canvas.");
} finally {
// Cleanup resources
if (chart) {
chart.destroy();
}
ctx = null; // Explicitly nullify for garbage collection
canvas = null; // Explicitly nullify for garbage collection
chartImage = null;
}
};
const enqueueRequest = (req, res, isSkia) => {
if (requestQueue.length >= CANVAS_QUEUE_LIMIT) {
res.status(503).send("Server is busy. Please try again later.");
return false;
}
requestQueue.push({ req, res, isSkia });
req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return true;
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res, isSkia } = requestQueue.shift();
try {
await processCanvasRequest(req, res, isSkia);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
}
isProcessing = false;
};
exports.canvastest = function (req, res) {
res.status(200).send("OK");
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, false)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};
exports.canvasSkia = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, true)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
}; };

View File

@@ -3,24 +3,36 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
}); });
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const inlineCssTool = require("inline-css"); //const inlineCssTool = require("inline-css");
const juice = require("juice");
exports.inlinecss = (req, res) => { exports.inlinecss = async (req, res) => {
//Perform request validation //Perform request validation
logger.log("email-inline-css", "DEBUG", req.user.email, null, null); logger.log("email-inline-css", "DEBUG", req.user.email, null, null);
const { html, url } = req.body; const { html, url } = req.body;
try {
inlineCssTool(html, { url: url }) const inlinedHtml = juice(html, {
.then((inlinedHtml) => { applyAttributesTableElements: false,
res.send(inlinedHtml); preserveMediaQueries: false,
}) applyWidthAttributes: false
.catch((error) => {
logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
error
});
res.send(error);
}); });
res.send(inlinedHtml);
} catch (error) {
logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
error
});
res.send(error.message);
}
// inlineCssTool(html, { url: url })
// .then((inlinedHtml) => {
// res.send(inlinedHtml);
// })
// .catch((error) => {
// logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
// error
// });
// });
}; };

View File

@@ -2,10 +2,12 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
const { inlinecss } = require("../render/inlinecss"); const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas } = require("../render/canvas-handler"); const { canvas, canvasSkia } = require("../render/canvas-handler");
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
// Define the route for inline CSS rendering // Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas); router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas);
router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia);
module.exports = router; module.exports = router;