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:
Dave Richer
2024-12-05 17:23:58 +00:00
8 changed files with 151 additions and 82 deletions

View File

@@ -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",

View File

@@ -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": "",

View File

@@ -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": "",

View File

@@ -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"
} }
} }
: {}), : {}),

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,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;

View File

@@ -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();
}; };

View File

@@ -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;