diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index 419de25fa..5de4231f7 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -25,6 +25,9 @@ export default function OwnerDetailFormComponent({ form, loading }) { + + + diff --git a/client/src/graphql/owners.queries.js b/client/src/graphql/owners.queries.js index e28fc8aa2..d169263f4 100644 --- a/client/src/graphql/owners.queries.js +++ b/client/src/graphql/owners.queries.js @@ -48,6 +48,7 @@ export const QUERY_OWNER_BY_ID = gql` query QUERY_OWNER_BY_ID($id: uuid!) { owners_by_pk(id: $id) { id + accountingid allow_text_message ownr_addr1 ownr_addr2 diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e86e9936e..337047e1f 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "Select an existing owner record or create a new one. " }, "fields": { + "accountingid": "Accounting ID", "address": "Address", "allow_text_message": "Permission to Text?", "name": "Name", @@ -3057,6 +3058,7 @@ "production_not_production_status": "Production not in Production Status", "production_over_time": "Production Level over Time", "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_summary": "Purchase & Return Ratio by Vendor (Summary)", "purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)", @@ -3082,6 +3084,7 @@ "timetickets": "Time Tickets", "timetickets_employee": "Employee Time Tickets", "timetickets_summary": "Time Tickets Summary", + "total_loss_jobs": "Jobs Marked as Total Loss", "unclaimed_hrs": "Unflagged Hours", "void_ros": "Void ROs", "work_in_progress_committed_labour": "Work in Progress - Committed Labor", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8b9026489..02c3dd2dd 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "" }, "fields": { + "accountingid": "", "address": "Dirección", "allow_text_message": "Permiso de texto?", "name": "Nombre", @@ -3057,6 +3058,7 @@ "production_not_production_status": "", "production_over_time": "", "psr_by_make": "", + "purchase_return_ratio_excel": "", "purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_summary": "", "purchases_by_cost_center_detail": "", @@ -3082,6 +3084,7 @@ "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", + "total_loss_jobs": "", "unclaimed_hrs": "", "void_ros": "", "work_in_progress_committed_labour": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8f9330ea0..7ea7ef02c 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2394,6 +2394,7 @@ "selectexistingornew": "" }, "fields": { + "accountingid": "", "address": "Adresse", "allow_text_message": "Autorisation de texte?", "name": "Prénom", @@ -3057,6 +3058,7 @@ "production_not_production_status": "", "production_over_time": "", "psr_by_make": "", + "purchase_return_ratio_excel": "", "purchase_return_ratio_grouped_by_vendor_detail": "", "purchase_return_ratio_grouped_by_vendor_summary": "", "purchases_by_cost_center_detail": "", @@ -3082,6 +3084,7 @@ "timetickets": "", "timetickets_employee": "", "timetickets_summary": "", + "total_loss_jobs": "", "unclaimed_hrs": "", "void_ros": "", "work_in_progress_committed_labour": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 9f80986d2..9e40624c0 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2184,6 +2184,30 @@ export const TemplateList = (type, context) => { }, group: "payroll", 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" } } : {}), diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index f57c6711a..c7c74928b 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -328,6 +328,7 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st }, + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}), ...(isThreeTier ? { Job: true, @@ -395,7 +396,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { PostalCode: job.ownr_zip, CountrySubDivisionCode: job.ownr_st }, - + ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}), Job: true, ParentRef: { 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(), Line2: job.ownr_addr1 || "", 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, { @@ -673,7 +675,8 @@ async function InsertInvoiceMultiPayerInvoice( Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(), Line2: job.ownr_addr1 || "", 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, { diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js index b35926935..946b6d082 100644 --- a/server/render/canvas-handler.js +++ b/server/render/canvas-handler.js @@ -5,6 +5,9 @@ const Chart = require("chart.js/auto"); const { backgroundColors, borderColors } = require("./canvas-colors"); const { defaultsDeep, isNumber } = require("lodash"); +let isProcessing = false; +const requestQueue = []; + try { FontLibrary.use("Montserrat", [ "/usr/share/fonts/Montserrat-Regular.ttf", @@ -60,69 +63,81 @@ const getChartConfiguration = (keys, values, override) => { return defaultsDeep(override || {}, defaultConfiguration); }; -exports.canvastest = function (req, res) { - res.status(200).send("OK"); -}; - -exports.canvas = function (req, res) { - const { logger } = req; - - const { w, h, values, keys, override } = req.body; - logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); - - // 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 = getChartConfiguration(keys, values, override); - - res.status(200).send( - (() => { - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - new Chart(ctx, configuration); - return canvas.toDataURL(); - })() - ); -}; - -exports.canvasSkia = async function (req, res) { +const processCanvasRequest = async (req, res, isSkia = false) => { const { logger } = req; const { w, h, values, keys, override } = req.body; - // Log incoming request for debugging logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override }); // Default width and height - const width = typeof w === "number" && w > 0 ? w : 500; - const height = typeof h === "number" && h > 0 ? h : 275; + const width = isNumber(w) && w > 0 ? w : 500; + const height = isNumber(h) && h > 0 ? h : 275; const configuration = getChartConfiguration(keys, values, override); try { - // Create a canvas and get the 2D rendering context - const canvas = new Canvas(width, height); + const canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height); const ctx = canvas.getContext("2d"); // Render the chart - new Chart(ctx, configuration); + const chart = new Chart(ctx, configuration); - // Convert the canvas to a Base64-encoded image - const chartImage = (await canvas.toBuffer("image/png")).toString("base64"); - const dataURL = `data:image/png;base64,${chartImage}`; + // Generate and send the image + const chartImage = isSkia + ? (await canvas.toBuffer("image/png")).toString("base64") + : canvas.toDataURL(); - // Send the Base64-encoded image as the response - res.status(200).send(dataURL); + chart.destroy(); + res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage); } catch (error) { - // Log and handle rendering errors logger.log("canvas-error", "error", "jsr", null, { error: error.message }); res.status(500).send("Failed to generate canvas."); } }; + +const processNextInQueue = async () => { + if (requestQueue.length === 0) { + isProcessing = false; + return; + } + + const { req, res, isSkia } = requestQueue.shift(); + await processCanvasRequest(req, res, isSkia); + 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, isSkia: false }); + req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); + return; + } + + isProcessing = true; + await processCanvasRequest(req, res, false); + processNextInQueue(); +}; + +exports.canvasSkia = 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, isSkia: true }); + req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); + return; + } + + isProcessing = true; + await processCanvasRequest(req, res, true); + processNextInQueue(); +};