Merged in release/2024-12-06 (pull request #1997)
Release/2024 12 06 into test-AIO IO-3051 IO-3050 IO-3042
This commit is contained in:
@@ -3084,6 +3084,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",
|
||||||
|
|||||||
@@ -3084,6 +3084,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": "",
|
||||||
|
|||||||
@@ -3084,6 +3084,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": "",
|
||||||
|
|||||||
@@ -2197,6 +2197,17 @@ export const TemplateList = (type, context) => {
|
|||||||
field: i18n.t("bills.fields.date")
|
field: i18n.t("bills.fields.date")
|
||||||
},
|
},
|
||||||
group: "purchases"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
20
server/middleware/validateCanvasRequestMiddleware.js
Normal file
20
server/middleware/validateCanvasRequestMiddleware.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const { isObject } = require("lodash");
|
||||||
|
|
||||||
|
const validateCanvasRequestMiddleware = (req, res, next) => {
|
||||||
|
const { w, h, values, keys, override } = req.body;
|
||||||
|
if (!values || !keys) {
|
||||||
|
return res.status(400).send("Missing required data");
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = validateCanvasRequestMiddleware;
|
||||||
@@ -5,37 +5,17 @@ const logger = require("../utils/logger");
|
|||||||
const { backgroundColors, borderColors } = require("./canvas-colors");
|
const { backgroundColors, borderColors } = require("./canvas-colors");
|
||||||
const { isObject, defaultsDeep, isNumber } = require("lodash");
|
const { isObject, defaultsDeep, isNumber } = require("lodash");
|
||||||
|
|
||||||
exports.canvastest = function (req, res) {
|
let isProcessing = false;
|
||||||
//console.log("Incoming test request.", req);
|
const requestQueue = [];
|
||||||
res.status(200).send("OK");
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.canvas = function (req, res) {
|
const processCanvasRequest = async (req, res) => {
|
||||||
|
try {
|
||||||
const { w, h, values, keys, override } = req.body;
|
const { w, h, values, keys, override } = req.body;
|
||||||
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
|
|
||||||
logger.log("inbound-canvas-creation", "debug", "jsr", null, { 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
|
// Set dimensions with defaults
|
||||||
if (override && !isObject(override)) {
|
const width = isNumber(w) ? w : 500;
|
||||||
res.status(400).send("Override must be an object");
|
const height = isNumber(h) ? h : 275;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the default Width and Height
|
|
||||||
let [width, height] = [500, 275];
|
|
||||||
|
|
||||||
// Allow for custom width and height
|
|
||||||
if (isNumber(w)) {
|
|
||||||
width = w;
|
|
||||||
}
|
|
||||||
if (isNumber(h)) {
|
|
||||||
height = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
type: "doughnut",
|
type: "doughnut",
|
||||||
@@ -51,6 +31,7 @@ exports.canvas = function (req, res) {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
animation: false,
|
||||||
devicePixelRatio: 4,
|
devicePixelRatio: 4,
|
||||||
responsive: false,
|
responsive: false,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: true,
|
||||||
@@ -82,12 +63,62 @@ exports.canvas = function (req, res) {
|
|||||||
return defaultsDeep(override, configuration);
|
return defaultsDeep(override, configuration);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.status(200).send(
|
// Generate chart
|
||||||
(() => {
|
let canvas = createCanvas(width, height);
|
||||||
const canvas = createCanvas(width, height);
|
let ctx = canvas.getContext("2d");
|
||||||
const ctx = canvas.getContext("2d");
|
let chart = new Chart(ctx, defaults());
|
||||||
new Chart(ctx, defaults());
|
const result = canvas.toDataURL();
|
||||||
return canvas.toDataURL();
|
|
||||||
})()
|
chart.destroy();
|
||||||
);
|
canvas.width = 0;
|
||||||
|
canvas.height = 0;
|
||||||
|
ctx = null;
|
||||||
|
canvas = null;
|
||||||
|
chart = null;
|
||||||
|
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
if (canvas) {
|
||||||
|
canvas.width = 0;
|
||||||
|
canvas.height = 0;
|
||||||
|
}
|
||||||
|
ctx = null;
|
||||||
|
canvas = null;
|
||||||
|
chart = null;
|
||||||
|
|
||||||
|
logger.log("inbound-canvas-creation", "error", "jsr", null, { error: error.message, stack: error.stack });
|
||||||
|
res.status(500).send("Error generating canvas");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processNextInQueue = async () => {
|
||||||
|
if (requestQueue.length === 0) {
|
||||||
|
isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { req, res } = requestQueue.shift();
|
||||||
|
await processCanvasRequest(req, res);
|
||||||
|
processNextInQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.canvastest = function (req, res) {
|
||||||
|
res.status(200).send("OK");
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.canvas = async function (req, res) {
|
||||||
|
if (isProcessing) {
|
||||||
|
if (requestQueue.length >= 100) {
|
||||||
|
// Set a maximum queue size
|
||||||
|
return res.status(503).send("Server is busy. Please try again later.");
|
||||||
|
}
|
||||||
|
requestQueue.push({ req, res });
|
||||||
|
logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
await processCanvasRequest(req, res);
|
||||||
|
processNextInQueue();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ 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 validateCanvasRequestMiddleware = require("../middleware/validateCanvasRequestMiddleware");
|
||||||
const { canvas } = require("../render/canvas-handler");
|
const { canvas } = require("../render/canvas-handler");
|
||||||
|
|
||||||
// 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, validateCanvasRequestMiddleware], canvas);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user