Compare commits
64 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cee795d70 | ||
|
|
b3c948f0c7 | ||
|
|
b955eb01b4 | ||
|
|
39640d254a | ||
|
|
532eb842b3 | ||
|
|
e1b00f5081 | ||
|
|
cfbf59cd48 | ||
|
|
6a09209659 | ||
|
|
cec5f6e6e7 | ||
|
|
82acaa35e1 | ||
|
|
09b8a05b5a | ||
|
|
83a1952880 | ||
|
|
e5d55f27b5 | ||
|
|
bfde72eed8 | ||
|
|
8fbd08d57f | ||
|
|
20bddb43b6 | ||
|
|
b38e0f611b | ||
|
|
c84fbcaba1 | ||
|
|
8f752d575a | ||
|
|
2fe9ae513d | ||
|
|
0001604552 | ||
|
|
5cb93b1a2c | ||
|
|
a04dcffc4c | ||
|
|
50c99f7a1e | ||
|
|
86f3179bc0 | ||
|
|
6336e7568f | ||
|
|
f0f199335c | ||
|
|
9c7c9f4b6d | ||
|
|
9001ceaed8 | ||
|
|
ab82e85c57 | ||
|
|
2effe5ef50 | ||
|
|
006a2a5dca | ||
|
|
a885bdec74 | ||
|
|
8d2bdb171b | ||
|
|
5d7eabbfa9 | ||
|
|
a2ada7d88e | ||
|
|
3a6af12446 | ||
|
|
b490ab96be | ||
|
|
ca462f51ec | ||
|
|
44721019fa | ||
|
|
8ed81e9aed | ||
|
|
15ba2a1caf | ||
|
|
aad22f2e2d | ||
|
|
7a11b18037 | ||
|
|
241322fa30 | ||
|
|
f0461270de | ||
|
|
11b906103a | ||
|
|
3f006f431e | ||
|
|
6f2b5e4c55 | ||
|
|
50d7c5dace | ||
|
|
9ac27b6090 | ||
|
|
51a1b48da9 | ||
|
|
7402679091 | ||
|
|
cb46ee5700 | ||
|
|
73af18f287 | ||
|
|
c3b184d17b | ||
|
|
4d35976241 | ||
|
|
5edbed3f0b | ||
|
|
3d79be06de | ||
|
|
fd9e7b4d4b | ||
|
|
2937a07379 | ||
|
|
6a7548d11b | ||
|
|
affbb3f168 | ||
|
|
0522747b49 |
79
.gitattributes
vendored
79
.gitattributes
vendored
@@ -1 +1,80 @@
|
|||||||
|
# Ensure all text files use LF for line endings
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
|
||||||
|
# Binary files should not be modified by Git
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg binary
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.eot binary
|
||||||
|
|
||||||
|
# Videos
|
||||||
|
*.mp4 binary
|
||||||
|
*.mov binary
|
||||||
|
*.avi binary
|
||||||
|
*.mkv binary
|
||||||
|
*.webm binary
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
*.mp3 binary
|
||||||
|
*.wav binary
|
||||||
|
*.ogg binary
|
||||||
|
*.flac binary
|
||||||
|
|
||||||
|
# Archives and compressed files
|
||||||
|
*.zip binary
|
||||||
|
*.gz binary
|
||||||
|
*.tar binary
|
||||||
|
*.7z binary
|
||||||
|
*.rar binary
|
||||||
|
|
||||||
|
# PDF and documents
|
||||||
|
*.pdf binary
|
||||||
|
*.doc binary
|
||||||
|
*.docx binary
|
||||||
|
*.xls binary
|
||||||
|
*.xlsx binary
|
||||||
|
*.ppt binary
|
||||||
|
*.pptx binary
|
||||||
|
|
||||||
|
# Exclude JSON and other data files from text processing, if necessary
|
||||||
|
*.json text
|
||||||
|
*.xml text
|
||||||
|
*.csv text
|
||||||
|
|
||||||
|
# Scripts and code files should maintain LF endings
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
|
*.rb text eol=lf
|
||||||
|
*.java text eol=lf
|
||||||
|
*.php text eol=lf
|
||||||
|
|
||||||
|
# Git configuration files
|
||||||
|
.gitattributes text eol=lf
|
||||||
|
.gitignore text eol=lf
|
||||||
|
*.gitattributes text eol=lf
|
||||||
|
|
||||||
|
# Exclude some other potential binary files
|
||||||
|
*.db binary
|
||||||
|
*.sqlite binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
|
|||||||
5
.platform/hooks/predeploy/01-install-dd.sh
Normal file
5
.platform/hooks/predeploy/01-install-dd.sh
Normal 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."
|
||||||
14
Dockerfile
14
Dockerfile
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
|
import { CopyFilled, DeleteFilled } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
|
import { Button, Card, Col, Form, Input, message, notification, Row, Space, Spin, Statistic } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -23,7 +23,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
),
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,7 +46,6 @@ const CardPaymentModalComponent = ({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [paymentLink, setPaymentLink] = useState();
|
const [paymentLink, setPaymentLink] = useState();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
|
||||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -48,24 +54,33 @@ const CardPaymentModalComponent = ({
|
|||||||
skip: !context?.jobid
|
skip: !context?.jobid
|
||||||
});
|
});
|
||||||
|
|
||||||
//Initialize the intellipay window.
|
const collectIPayFields = () => {
|
||||||
|
const iPayFields = document.querySelectorAll(".ipayfield");
|
||||||
|
const iPayData = {};
|
||||||
|
iPayFields.forEach((field) => {
|
||||||
|
iPayData[field.dataset.ipayname] = field.value;
|
||||||
|
});
|
||||||
|
return iPayData;
|
||||||
|
};
|
||||||
|
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
|
|
||||||
window.intellipay.runOnClose(() => {
|
window.intellipay.runOnClose(() => {
|
||||||
//window.intellipay.initialize();
|
//window.intellipay.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
window.intellipay.runOnApproval(() => {
|
||||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||||
//Add a slight delay to allow the refetch to properly get the data.
|
//Add a slight delay to allow the refetch to properly get the data.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
|
if (actions?.refetch) actions.refetch();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}, 750);
|
}, 750);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
window.intellipay.runOnNonApproval(async (response) => {
|
||||||
// Mutate unsuccessful payment
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
@@ -98,16 +113,21 @@ const CardPaymentModalComponent = ({
|
|||||||
//Validate
|
//Validate
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await form.validateFields();
|
||||||
} catch (error) {
|
} catch {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iPayData = collectIPayFields();
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
bodyshop,
|
bodyshop,
|
||||||
refresh: !!window.intellipay,
|
refresh: !!window.intellipay,
|
||||||
paymentSplitMeta: form.getFieldsValue()
|
paymentSplitMeta: form.getFieldsValue(),
|
||||||
|
iPayData: iPayData,
|
||||||
|
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email }))
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.intellipay) {
|
if (window.intellipay) {
|
||||||
@@ -116,8 +136,8 @@ const CardPaymentModalComponent = ({
|
|||||||
SetIntellipayCallbackFunctions();
|
SetIntellipayCallbackFunctions();
|
||||||
window.intellipay.autoOpen();
|
window.intellipay.autoOpen();
|
||||||
} else {
|
} else {
|
||||||
var rg = document.createRange();
|
const rg = document.createRange();
|
||||||
let node = rg.createContextualFragment(response.data);
|
const node = rg.createContextualFragment(response.data);
|
||||||
document.documentElement.appendChild(node);
|
document.documentElement.appendChild(node);
|
||||||
SetIntellipayCallbackFunctions();
|
SetIntellipayCallbackFunctions();
|
||||||
window.intellipay.isAutoOpen = true;
|
window.intellipay.isAutoOpen = true;
|
||||||
@@ -137,25 +157,27 @@ const CardPaymentModalComponent = ({
|
|||||||
//Validate
|
//Validate
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await form.validateFields();
|
||||||
} catch (error) {
|
} catch {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iPayData = collectIPayFields();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
const response = await axios.post("/intellipay/generate_payment_url", {
|
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||||
bodyshop,
|
bodyshop,
|
||||||
amount: payments?.reduce((acc, val) => {
|
amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0),
|
||||||
return acc + (val?.amount || 0);
|
account: payments && data?.jobs?.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
||||||
}, 0),
|
|
||||||
account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
|
||||||
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
||||||
paymentSplitMeta: form.getFieldsValue()
|
paymentSplitMeta: form.getFieldsValue(),
|
||||||
|
iPayData: iPayData
|
||||||
});
|
});
|
||||||
if (response.data) {
|
|
||||||
setPaymentLink(response.data?.shorUrl);
|
if (response?.data?.shorUrl) {
|
||||||
navigator.clipboard.writeText(response.data?.shorUrl);
|
setPaymentLink(response.data.shorUrl);
|
||||||
|
await navigator.clipboard.writeText(response.data.shorUrl);
|
||||||
message.success(t("general.actions.copied"));
|
message.success(t("general.actions.copied"));
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -179,67 +201,44 @@ const CardPaymentModalComponent = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.List name={["payments"]}>
|
<Form.List name={["payments"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove }) => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
{fields.map((field, index) => (
|
||||||
{fields.map((field, index) => (
|
<Form.Item key={field.key}>
|
||||||
<Form.Item key={field.key}>
|
<Row gutter={[16, 16]}>
|
||||||
<Row gutter={[16, 16]}>
|
<Col span={16}>
|
||||||
<Col span={16}>
|
<Form.Item
|
||||||
<Form.Item
|
key={`${index}jobid`}
|
||||||
key={`${index}jobid`}
|
label={t("jobs.fields.ro_number")}
|
||||||
label={t("jobs.fields.ro_number")}
|
name={[field.name, "jobid"]}
|
||||||
name={[field.name, "jobid"]}
|
rules={[{ required: true }]}
|
||||||
rules={[
|
>
|
||||||
{
|
<JobSearchSelectComponent notExported={false} clm_no />
|
||||||
required: true
|
</Form.Item>
|
||||||
//message: t("general.validation.required"),
|
</Col>
|
||||||
}
|
<Col span={6}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
key={`${index}amount`}
|
||||||
<JobSearchSelectComponent notExported={false} clm_no />
|
label={t("payments.fields.amount")}
|
||||||
</Form.Item>
|
name={[field.name, "amount"]}
|
||||||
</Col>
|
rules={[{ required: true }]}
|
||||||
<Col span={6}>
|
>
|
||||||
<Form.Item
|
<CurrencyFormItemComponent />
|
||||||
key={`${index}amount`}
|
</Form.Item>
|
||||||
label={t("payments.fields.amount")}
|
</Col>
|
||||||
name={[field.name, "amount"]}
|
<Col span={2}>
|
||||||
rules={[
|
<DeleteFilled style={{ margin: "1rem" }} onClick={() => remove(field.name)} />
|
||||||
{
|
</Col>
|
||||||
required: true
|
</Row>
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyFormItemComponent />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<DeleteFilled
|
|
||||||
style={{ margin: "1rem" }}
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
))}
|
||||||
);
|
<Form.Item>
|
||||||
}}
|
<Button type="dashed" onClick={() => add()} style={{ width: "100%" }}>
|
||||||
|
{t("general.actions.add")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -283,9 +282,7 @@ const CardPaymentModalComponent = ({
|
|||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
const totalAmountToCharge = payments?.reduce((acc, val) => acc + (val?.amount || 0), 0);
|
||||||
return acc + (val?.amount || 0);
|
|
||||||
}, 0);
|
|
||||||
return (
|
return (
|
||||||
<Space style={{ float: "right" }}>
|
<Space style={{ float: "right" }}>
|
||||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||||
|
|||||||
@@ -334,29 +334,75 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "tag-added":
|
case "tag-added":
|
||||||
client.cache.modify({
|
// Ensure `job_conversations` is properly formatted
|
||||||
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
const formattedJobConversations = job_conversations.map((jc) => ({
|
||||||
fields: {
|
__typename: "job_conversations",
|
||||||
job_conversations: (existing = []) => [...existing, ...job_conversations]
|
jobid: jc.jobid || jc.job?.id,
|
||||||
|
conversationid: conversationId,
|
||||||
|
job: jc.job || {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: data.selectedJob.id,
|
||||||
|
ro_number: data.selectedJob.ro_number,
|
||||||
|
ownr_co_nm: data.selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: data.selectedJob.ownr_fn,
|
||||||
|
ownr_ln: data.selectedJob.ownr_ln
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
break;
|
|
||||||
|
|
||||||
case "tag-removed":
|
|
||||||
client.cache.modify({
|
client.cache.modify({
|
||||||
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
fields: {
|
fields: {
|
||||||
job_conversations: (existing = [], { readField }) => {
|
job_conversations: (existing = []) => {
|
||||||
return existing.filter((jobRef) => {
|
// Ensure no duplicates based on both `conversationid` and `jobid`
|
||||||
// Read the `jobid` field safely, even if the structure is normalized
|
const existingLinks = new Set(
|
||||||
const jobId = readField("jobid", jobRef);
|
existing.map((jc) => {
|
||||||
return jobId !== fields.jobId;
|
const jobId = client.cache.readFragment({
|
||||||
|
id: client.cache.identify(jc),
|
||||||
|
fragment: gql`
|
||||||
|
fragment JobConversationLinkAdded on job_conversations {
|
||||||
|
jobid
|
||||||
|
conversationid
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})?.jobid;
|
||||||
|
return `${jobId}:${conversationId}`; // Unique identifier for a job-conversation link
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const newItems = formattedJobConversations.filter((jc) => {
|
||||||
|
const uniqueLink = `${jc.jobid}:${jc.conversationid}`;
|
||||||
|
return !existingLinks.has(uniqueLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return [...existing, ...newItems];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "tag-removed":
|
||||||
|
try {
|
||||||
|
const conversationCacheId = client.cache.identify({ __typename: "conversations", id: conversationId });
|
||||||
|
|
||||||
|
// Evict the specific cache entry for job_conversations
|
||||||
|
client.cache.evict({
|
||||||
|
id: conversationCacheId,
|
||||||
|
fieldName: "job_conversations"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Garbage collect evicted entries
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - tag removed - Refetched conversation list after state change", {
|
||||||
|
conversationId,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refetching queries after conversation state change: (Tag Removed)", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logLocal("handleConversationChanged - Unhandled type", { type });
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
client.cache.modify({
|
client.cache.modify({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { renderMessage } from "./renderMessage";
|
import { renderMessage } from "./renderMessage";
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
@@ -16,7 +16,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
loadedImagesRef.current = 0;
|
loadedImagesRef.current = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const preloadImages = (imagePaths, onComplete) => {
|
const preloadImages = useCallback((imagePaths, onComplete) => {
|
||||||
resetImageLoadState();
|
resetImageLoadState();
|
||||||
|
|
||||||
if (imagePaths.length === 0) {
|
if (imagePaths.length === 0) {
|
||||||
@@ -34,7 +34,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// Ensure all images are loaded on initial render
|
// Ensure all images are loaded on initial render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,7 +51,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [messages]);
|
}, [messages, preloadImages]);
|
||||||
|
|
||||||
// Handle scrolling when new messages are added
|
// Handle scrolling when new messages are added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -69,7 +69,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [messages, atBottom]);
|
}, [messages, atBottom, preloadImages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient, useLazyQuery } from "@apollo/client";
|
import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
|
import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
|
||||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
|
||||||
@@ -38,6 +38,14 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Query for unread count when chat is not visible
|
||||||
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: chatVisible, // Skip when chat is visible
|
||||||
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
|
});
|
||||||
|
|
||||||
// Socket connection status
|
// Socket connection status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSocketStatus = () => {
|
const handleSocketStatus = () => {
|
||||||
@@ -77,23 +85,29 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
|
|
||||||
// Get unread count from the cache
|
// Get unread count from the cache
|
||||||
const unreadCount = (() => {
|
const unreadCount = (() => {
|
||||||
try {
|
if (chatVisible) {
|
||||||
const cachedData = client.readQuery({
|
try {
|
||||||
query: CONVERSATION_LIST_QUERY,
|
const cachedData = client.readQuery({
|
||||||
variables: { offset: 0 }
|
query: CONVERSATION_LIST_QUERY,
|
||||||
});
|
variables: { offset: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
if (!cachedData?.conversations) return 0;
|
if (!cachedData?.conversations) return 0;
|
||||||
|
|
||||||
// Aggregate unread message count
|
// Aggregate unread message count
|
||||||
return cachedData.conversations.reduce((total, conversation) => {
|
return cachedData.conversations.reduce((total, conversation) => {
|
||||||
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
||||||
return total + unread;
|
return total + unread;
|
||||||
}, 0);
|
}, 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Unread count not found in cache:", error);
|
console.warn("Unread count not found in cache:", error);
|
||||||
return 0; // Fallback if not in cache
|
return 0; // Fallback if not in cache
|
||||||
|
}
|
||||||
|
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
|
||||||
|
// Use the unread count from the query result
|
||||||
|
return unreadData.messages_aggregate.aggregate.count;
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -52,20 +52,26 @@ export function ChatTagRoContainer({ conversation, bodyshop }) {
|
|||||||
// Find the job details from the search data
|
// Find the job details from the search data
|
||||||
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
|
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
|
||||||
if (!selectedJob) return;
|
if (!selectedJob) return;
|
||||||
const newJobConversation = {
|
|
||||||
__typename: "job_conversations",
|
|
||||||
jobid: selectedJob.id,
|
|
||||||
conversationid: conversation.id,
|
|
||||||
job: {
|
|
||||||
__typename: "jobs",
|
|
||||||
...selectedJob
|
|
||||||
}
|
|
||||||
};
|
|
||||||
socket.emit("conversation-modified", {
|
socket.emit("conversation-modified", {
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
bodyshopId: bodyshop.id,
|
bodyshopId: bodyshop.id,
|
||||||
type: "tag-added",
|
type: "tag-added",
|
||||||
job_conversations: [newJobConversation]
|
selectedJob,
|
||||||
|
job_conversations: [
|
||||||
|
{
|
||||||
|
__typename: "job_conversations",
|
||||||
|
jobid: selectedJob.id,
|
||||||
|
conversationid: conversation.id,
|
||||||
|
job: {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: selectedJob.id,
|
||||||
|
ro_number: selectedJob.ro_number,
|
||||||
|
ownr_co_nm: selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: selectedJob.ownr_fn,
|
||||||
|
ownr_ln: selectedJob.ownr_ln
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { gql, useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client";
|
import { gql, useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Col, Row, notification } from "antd";
|
import { Col, Row, notification } from "antd"; //import { Button, Col, Row, notification } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -408,8 +408,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
updateSchComp={updateSchComp}
|
updateSchComp={updateSchComp}
|
||||||
setSchComp={setSchComp}
|
setSchComp={setSchComp}
|
||||||
/>
|
/>
|
||||||
{
|
{/* {
|
||||||
{/* currentUser.email.includes("@rome.") || currentUser.email.includes("@imex.") ? (
|
currentUser.email.includes("@rome.") || currentUser.email.includes("@imex.") ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
for (const record of data.available_jobs) {
|
for (const record of data.available_jobs) {
|
||||||
@@ -425,8 +425,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
>
|
>
|
||||||
Add all jobs as new.
|
Add all jobs as new.
|
||||||
</Button>
|
</Button>
|
||||||
) : null */}
|
) : null
|
||||||
}
|
} */}
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<JobsAvailableTableComponent
|
<JobsAvailableTableComponent
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -283,7 +283,12 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (value && value[0] && value[1] && process.env.NODE_ENV === "production") {
|
if (
|
||||||
|
(!import.meta.env.VITE_APP_IS_TEST && import.meta.env.PROD) &&
|
||||||
|
value &&
|
||||||
|
value[0] &&
|
||||||
|
value[1]
|
||||||
|
) {
|
||||||
const diffInDays = (value[1] - value[0]) / (1000 * 3600 * 24);
|
const diffInDays = (value[1] - value[0]) / (1000 * 3600 * 24);
|
||||||
if (diffInDays > 92) {
|
if (diffInDays > 92) {
|
||||||
return Promise.reject(t("general.validation.dateRangeExceeded"));
|
return Promise.reject(t("general.validation.dateRangeExceeded"));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2394,6 +2394,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",
|
||||||
@@ -3057,6 +3058,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)",
|
||||||
@@ -3082,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",
|
||||||
|
|||||||
@@ -2394,6 +2394,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",
|
||||||
@@ -3057,6 +3058,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": "",
|
||||||
@@ -3082,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": "",
|
||||||
|
|||||||
@@ -2394,6 +2394,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",
|
||||||
@@ -3057,6 +3058,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": "",
|
||||||
@@ -3082,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": "",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { setContext } from "@apollo/client/link/context";
|
|||||||
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
|
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
|
||||||
import { RetryLink } from "@apollo/client/link/retry";
|
import { RetryLink } from "@apollo/client/link/retry";
|
||||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||||
import { getMainDefinition, offsetLimitPagination } from "@apollo/client/utilities";
|
import { getMainDefinition } from "@apollo/client/utilities";
|
||||||
//import { split } from "apollo-link";
|
//import { split } from "apollo-link";
|
||||||
import apolloLogger from "apollo-link-logger";
|
import apolloLogger from "apollo-link-logger";
|
||||||
//import axios from "axios";
|
//import axios from "axios";
|
||||||
@@ -143,36 +143,7 @@ middlewares.push(
|
|||||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
||||||
);
|
);
|
||||||
|
|
||||||
const cache = new InMemoryCache({
|
const cache = new InMemoryCache({});
|
||||||
typePolicies: {
|
|
||||||
conversations: {
|
|
||||||
fields: {
|
|
||||||
job_conversations: {
|
|
||||||
merge(existing = [], incoming = [], { readField }) {
|
|
||||||
const merged = new Map();
|
|
||||||
|
|
||||||
// Add existing data to the map
|
|
||||||
existing.forEach((jobConversation) => {
|
|
||||||
// Use `readField` to get the unique `jobid`, fallback to `__ref`
|
|
||||||
const jobId = readField("jobid", jobConversation) || jobConversation.__ref;
|
|
||||||
if (jobId) merged.set(jobId, jobConversation);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add or replace with incoming data
|
|
||||||
incoming.forEach((jobConversation) => {
|
|
||||||
// Use `readField` to get the unique `jobid`, fallback to `__ref`
|
|
||||||
const jobId = readField("jobid", jobConversation) || jobConversation.__ref;
|
|
||||||
if (jobId) merged.set(jobId, jobConversation);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the merged data as an array
|
|
||||||
return Array.from(merged.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@@ -2184,6 +2184,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
847
package-lock.json
generated
847
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 ? { PrimaryEmailAddr: { 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 ? { PrimaryEmailAddr: { 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, {
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const qs = require("query-string");
|
const qs = require("query-string");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
|
||||||
const { sendTaskEmail } = require("../email/sendemail");
|
const { sendTaskEmail } = require("../email/sendemail");
|
||||||
const generateEmailTemplate = require("../email/generateTemplate");
|
const generateEmailTemplate = require("../email/generateTemplate");
|
||||||
const { getEndpoints } = require("../email/tasksEmails");
|
const { getEndpoints } = require("../email/tasksEmails");
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const domain = process.env.NODE_ENV ? "secure" : "secure";
|
const domain = process.env.NODE_ENV ? "secure" : "test";
|
||||||
|
|
||||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||||
const { InstanceRegion } = require("../utils/instanceMgr");
|
const { InstanceRegion } = require("../utils/instanceMgr");
|
||||||
@@ -52,15 +50,43 @@ const getShopCredentials = async (bodyshop) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const decodeComment = (comment) => {
|
||||||
|
try {
|
||||||
|
return comment ? JSON.parse(Buffer.from(comment, "base64").toString()) : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null; // Handle malformed base64 string gracefully
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.lightbox_credentials = async (req, res) => {
|
exports.lightbox_credentials = async (req, res) => {
|
||||||
logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, null);
|
const decodedComment = decodeComment(req.body?.comment);
|
||||||
|
const logMeta = {
|
||||||
|
iPayData: req.body?.iPayData,
|
||||||
|
decodedComment,
|
||||||
|
bodyshop: {
|
||||||
|
id: req.body?.bodyshop?.id,
|
||||||
|
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||||
|
name: req.body?.bodyshop?.shopname
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, logMeta);
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
if (shopCredentials.error) {
|
if (shopCredentials.error) {
|
||||||
res.json(shopCredentials);
|
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: shopCredentials.error?.message,
|
||||||
|
...logMeta
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
message: shopCredentials.error?.message,
|
||||||
|
type: "intellipay-credentials-error",
|
||||||
|
...logMeta
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -74,26 +100,61 @@ exports.lightbox_credentials = async (req, res) => {
|
|||||||
|
|
||||||
const response = await axios(options);
|
const response = await axios(options);
|
||||||
|
|
||||||
|
logger.log("intellipay-lightbox-success", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
...logMeta
|
||||||
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.log(error);
|
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, {
|
||||||
logger.log("intellipay-lightbox-credentials-error", "ERROR", req.user?.email, null, {
|
message: error?.message,
|
||||||
error: JSON.stringify(error)
|
...logMeta
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
message: error?.message,
|
||||||
|
type: "intellipay-lightbox-error",
|
||||||
|
...logMeta
|
||||||
});
|
});
|
||||||
res.json({ error });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.payment_refund = async (req, res) => {
|
exports.payment_refund = async (req, res) => {
|
||||||
logger.log("intellipay-refund", "DEBUG", req.user?.email, null, null);
|
const decodedComment = decodeComment(req.body.iPayData?.comment);
|
||||||
|
const logResponseMeta = {
|
||||||
|
iPayData: req.body?.iPayData,
|
||||||
|
bodyshop: {
|
||||||
|
id: req.body.bodyshop?.id,
|
||||||
|
imexshopid: req.body.bodyshop?.imexshopid,
|
||||||
|
name: req.body.bodyshop?.shopname
|
||||||
|
},
|
||||||
|
paymentid: req.body?.paymentid,
|
||||||
|
amount: req.body?.amount,
|
||||||
|
decodedComment
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-refund-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
|
if (shopCredentials.error) {
|
||||||
|
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
|
credentialsError: shopCredentials.error,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(400).json({
|
||||||
|
credentialsError: shopCredentials.error,
|
||||||
|
type: "intellipay-refund-credentials-error",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
|
|
||||||
data: qs.stringify({
|
data: qs.stringify({
|
||||||
method: "payment_refund",
|
method: "payment_refund",
|
||||||
...shopCredentials,
|
...shopCredentials,
|
||||||
@@ -103,132 +164,255 @@ exports.payment_refund = async (req, res) => {
|
|||||||
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`
|
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
const response = await axios(options);
|
const response = await axios(options);
|
||||||
|
|
||||||
|
logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.log(error);
|
|
||||||
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
|
||||||
error: JSON.stringify(error)
|
message: error?.message,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.status(500).json({
|
||||||
|
message: error?.message,
|
||||||
|
type: "intellipay-refund-error",
|
||||||
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({ error });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.generate_payment_url = async (req, res) => {
|
exports.generate_payment_url = async (req, res) => {
|
||||||
logger.log("intellipay-payment-url", "DEBUG", req.user?.email, null, null);
|
const decodedComment = decodeComment(req.body.comment);
|
||||||
|
const logResponseMeta = {
|
||||||
|
iPayData: req.body?.iPayData,
|
||||||
|
bodyshop: {
|
||||||
|
id: req.body.bodyshop?.id,
|
||||||
|
imexshopid: req.body.bodyshop?.imexshopid,
|
||||||
|
name: req.body.bodyshop?.shopname
|
||||||
|
},
|
||||||
|
amount: req.body?.amount,
|
||||||
|
account: req.body?.account,
|
||||||
|
comment: req.body?.comment,
|
||||||
|
invoice: req.body?.invoice,
|
||||||
|
decodedComment
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-generate-payment-url-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
|
if (shopCredentials.error) {
|
||||||
|
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: shopCredentials.error?.message,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.status(400).json({
|
||||||
|
message: shopCredentials.error?.message,
|
||||||
|
type: "intellipay-generate-payment-url-credentials-error",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
//TODO: Move these to environment variables/database.
|
|
||||||
data: qs.stringify({
|
data: qs.stringify({
|
||||||
...shopCredentials,
|
...shopCredentials,
|
||||||
//...req.body,
|
|
||||||
amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"),
|
amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"),
|
||||||
account: req.body.account,
|
account: req.body.account,
|
||||||
comment: req.body.comment,
|
comment: req.body.comment,
|
||||||
invoice: req.body.invoice,
|
invoice: req.body.invoice,
|
||||||
createshorturl: true
|
createshorturl: true
|
||||||
//The postback URL is set at the CP teller global terminal settings page.
|
|
||||||
}),
|
}),
|
||||||
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`
|
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
const response = await axios(options);
|
const response = await axios(options);
|
||||||
|
|
||||||
|
logger.log("intellipay-generate-payment-url-success", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
shortUrl: response.data?.shorturl,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
res.send(response.data);
|
res.send(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.log(error);
|
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
|
||||||
logger.log("intellipay-payment-url-error", "ERROR", req.user?.email, null, {
|
message: error?.message,
|
||||||
error: JSON.stringify(error)
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.json({ error });
|
res.status(500).json({ message: error?.message, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
||||||
exports.checkfee = async (req, res) => {
|
exports.checkfee = async (req, res) => {
|
||||||
// Requires amount, bodyshop.imexshopid, and state? to get data.
|
const logResponseMeta = {
|
||||||
logger.log("intellipay-fee-check", "DEBUG", req.user?.email, null, null);
|
bodyshop: {
|
||||||
|
id: req.body?.bodyshop?.id,
|
||||||
|
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||||
|
name: req.body?.bodyshop?.shopname,
|
||||||
|
state: req.body?.bodyshop?.state
|
||||||
|
},
|
||||||
|
amount: req.body?.amount
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
//If there's no amount, there can't be a fee. Skip the call.
|
|
||||||
if (!req.body.amount || req.body.amount <= 0) {
|
if (!req.body.amount || req.body.amount <= 0) {
|
||||||
|
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
||||||
|
message: "Amount is zero or undefined, skipping fee check.",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
res.json({ fee: 0 });
|
res.json({ fee: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
|
if (shopCredentials.error) {
|
||||||
|
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: shopCredentials.error?.message,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
//TODO: Move these to environment variables/database.
|
|
||||||
data: qs.stringify(
|
data: qs.stringify(
|
||||||
{
|
{
|
||||||
method: "fee",
|
method: "fee",
|
||||||
...shopCredentials,
|
...shopCredentials,
|
||||||
amount: req.body.amount,
|
amount: req.body.amount,
|
||||||
paymenttype: `CC`,
|
paymenttype: `CC`,
|
||||||
cardnum: "4111111111111111", //Not needed per documentation, but incorrect values come back without it.
|
cardnum: "4111111111111111", // Required for compatibility with API
|
||||||
state:
|
state:
|
||||||
req.body.bodyshop?.state && req.body.bodyshop.state?.length === 2
|
req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
|
||||||
? req.body.bodyshop.state.toUpperCase()
|
? req.body.bodyshop.state.toUpperCase()
|
||||||
: "ZZ" //Same as above
|
: "ZZ"
|
||||||
},
|
},
|
||||||
{ sort: false } //ColdFusion Query Strings depend on order. This preserves it.
|
{ sort: false } // Ensure query string order is preserved
|
||||||
),
|
),
|
||||||
url: `https://${domain}.cpteller.com/api/26/webapi.cfc`
|
url: `https://${domain}.cpteller.com/api/26/webapi.cfc`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, {
|
||||||
|
requestOptions: options,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
const response = await axios(options);
|
const response = await axios(options);
|
||||||
|
|
||||||
if (response.data?.error) {
|
if (response.data?.error) {
|
||||||
res.status(400).json({ error: response.data.error });
|
logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, {
|
||||||
|
message: response.data?.error,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.status(400).json({
|
||||||
|
error: response.data?.error,
|
||||||
|
type: "intellipay-checkfee-api-error",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
} else if (response.data < 0) {
|
} else if (response.data < 0) {
|
||||||
res.json({ error: "Fee amount negative. Check API credentials & account configuration." });
|
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||||
|
message: "Fee amount returned is negative.",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.json({
|
||||||
|
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||||
|
...logResponseMeta,
|
||||||
|
type: "intellipay-checkfee-negative-fee"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res.json({ fee: response.data });
|
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
||||||
|
fee: response.data,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
res.json({ fee: response.data, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.log(error);
|
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
||||||
logger.log("intellipay-fee-check-error", "ERROR", req.user?.email, null, {
|
message: error?.message,
|
||||||
error: error.message
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({ error });
|
res.status(500).json({ error: error?.message, logResponseMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.postback = async (req, res) => {
|
exports.postback = async (req, res) => {
|
||||||
|
const { body: values } = req;
|
||||||
|
const decodedComment = decodeComment(values?.comment);
|
||||||
|
const logResponseMeta = {
|
||||||
|
bodyshop: {
|
||||||
|
id: req.body?.bodyshop?.id,
|
||||||
|
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||||
|
name: req.body?.bodyshop?.shopname,
|
||||||
|
state: req.body?.bodyshop?.state
|
||||||
|
},
|
||||||
|
iprequest: values,
|
||||||
|
decodedComment
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body);
|
if ((!values.invoice || values.invoice === "") && !decodedComment) {
|
||||||
const { body: values } = req;
|
|
||||||
|
|
||||||
const comment = Buffer.from(values?.comment, "base64").toString();
|
|
||||||
|
|
||||||
if ((!values.invoice || values.invoice === "") && !comment) {
|
|
||||||
//invoice is specified through the pay link. Comment by IO.
|
//invoice is specified through the pay link. Comment by IO.
|
||||||
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, req.body);
|
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, {
|
||||||
|
message: "No invoice or comment provided",
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment) {
|
if (decodedComment) {
|
||||||
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
|
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
|
||||||
//This has been triggered by IO and may have multiple jobs.
|
//This has been triggered by IO and may have multiple jobs.
|
||||||
const parsedComment = JSON.parse(comment);
|
const parsedComment = JSON.parse(decodedComment);
|
||||||
|
|
||||||
|
logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, {
|
||||||
|
parsedComment,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
//Adding in the user email to the short pay email.
|
//Adding in the user email to the short pay email.
|
||||||
//Need to check this to ensure backwards compatibility for clients that don't update.
|
//Need to check this to ensure backwards compatibility for clients that don't update.
|
||||||
|
|
||||||
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
|
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
|
||||||
|
|
||||||
|
// Fetch jobs by job IDs
|
||||||
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
|
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
|
||||||
ids: partialPayments.map((p) => p.jobid)
|
ids: partialPayments.map((p) => p.jobid)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.log("intellipay-postback-jobs-fetched", "DEBUG", req.user?.email, null, {
|
||||||
|
jobs,
|
||||||
|
parsedComment,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert new payments
|
||||||
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
||||||
paymentInput: partialPayments.map((p) => ({
|
paymentInput: partialPayments.map((p) => ({
|
||||||
amount: p.amount,
|
amount: p.amount,
|
||||||
@@ -250,13 +434,15 @@ exports.postback = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, JSON.stringify(jobs), {
|
|
||||||
iprequest: values,
|
logger.log("intellipay-postback-payment-success", "DEBUG", req.user?.email, null, {
|
||||||
paymentResult
|
paymentResult,
|
||||||
|
jobs,
|
||||||
|
parsedComment,
|
||||||
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
|
|
||||||
if (values.origin === "OneLink" && parsedComment.userEmail) {
|
if (values.origin === "OneLink" && parsedComment.userEmail) {
|
||||||
//Send an email, it was a text to pay link.
|
|
||||||
try {
|
try {
|
||||||
const endPoints = getEndpoints();
|
const endPoints = getEndpoints();
|
||||||
sendTaskEmail({
|
sendTaskEmail({
|
||||||
@@ -275,20 +461,25 @@ exports.postback = async (req, res) => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-postback-app-email-error", "DEBUG", req.user?.email, JSON.stringify(jobs), {
|
logger.log("intellipay-postback-email-error", "ERROR", req.user?.email, null, {
|
||||||
iprequest: values,
|
message: error.message,
|
||||||
|
jobs,
|
||||||
paymentResult,
|
paymentResult,
|
||||||
error: error.message
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} else if (values.invoice) {
|
} else if (values.invoice) {
|
||||||
//This is a link email that's been sent out.
|
|
||||||
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
|
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
|
||||||
id: values.invoice
|
id: values.invoice
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", req.user?.email, null, {
|
||||||
|
job,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
|
||||||
paymentInput: {
|
paymentInput: {
|
||||||
amount: values.total,
|
amount: values.total,
|
||||||
@@ -300,6 +491,11 @@ exports.postback = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", req.user?.email, null, {
|
||||||
|
paymentResult,
|
||||||
|
...logResponseMeta
|
||||||
|
});
|
||||||
|
|
||||||
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
|
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
|
||||||
paymentResponse: {
|
paymentResponse: {
|
||||||
amount: values.total,
|
amount: values.total,
|
||||||
@@ -313,18 +509,17 @@ exports.postback = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, values.invoice, {
|
logger.log("intellipay-postback-invoice-response-success", "DEBUG", req.user?.email, null, {
|
||||||
iprequest: values,
|
|
||||||
responseResults,
|
responseResults,
|
||||||
paymentResult
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("intellipay-postback-total-error", "ERROR", req.user?.email, null, {
|
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {
|
||||||
error: JSON.stringify(error),
|
message: error?.message,
|
||||||
body: req.body
|
...logResponseMeta
|
||||||
});
|
});
|
||||||
res.status(400).json({ succesful: false, error: error.message });
|
res.status(400).json({ successful: false, error: error.message, ...logResponseMeta });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -857,8 +857,8 @@ function GenerateCostingData(job) {
|
|||||||
summaryData.totalSales = summaryData.totalSales.add(Adjustment);
|
summaryData.totalSales = summaryData.totalSales.add(Adjustment);
|
||||||
//Add to lines.
|
//Add to lines.
|
||||||
costCenterData.push({
|
costCenterData.push({
|
||||||
id: "Adj",
|
id: "AdjEst",
|
||||||
cost_center: "Adjustment",
|
cost_center: "Adjustment (Est. Match)",
|
||||||
sale_labor: Dinero().toFormat(),
|
sale_labor: Dinero().toFormat(),
|
||||||
sale_labor_dinero: Dinero(),
|
sale_labor_dinero: Dinero(),
|
||||||
sale_parts: Dinero().toFormat(),
|
sale_parts: Dinero().toFormat(),
|
||||||
|
|||||||
@@ -73,7 +73,16 @@ async function TotalsServerSide(req, res) {
|
|||||||
job.cieca_ttl.data.n_ttl_amt === job.cieca_ttl.data.g_ttl_amt //It looks like sometimes, gross and net are the same, but they shouldn't be.
|
job.cieca_ttl.data.n_ttl_amt === job.cieca_ttl.data.g_ttl_amt //It looks like sometimes, gross and net are the same, but they shouldn't be.
|
||||||
? job.cieca_ttl.data.n_ttl_amt - job.cieca_ttl.data.g_tax
|
? job.cieca_ttl.data.n_ttl_amt - job.cieca_ttl.data.g_tax
|
||||||
: job.cieca_ttl.data.g_ttl_amt - job.cieca_ttl.data.g_tax; //If they are, adjust the gross total down by the tax amount.
|
: job.cieca_ttl.data.g_ttl_amt - job.cieca_ttl.data.g_tax; //If they are, adjust the gross total down by the tax amount.
|
||||||
const ttlDifference = emsTotal - ret.totals.subtotal.getAmount() / 100;
|
const ttlDifference =
|
||||||
|
emsTotal -
|
||||||
|
ret.totals.subtotal
|
||||||
|
.add(
|
||||||
|
Dinero({
|
||||||
|
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
|
||||||
|
}).multiply(-1) //Add back in the adjustment to the subtotal. We don't want to scrub it twice.
|
||||||
|
)
|
||||||
|
.getAmount() /
|
||||||
|
100;
|
||||||
|
|
||||||
if (Math.abs(ttlDifference) > 0.0) {
|
if (Math.abs(ttlDifference) > 0.0) {
|
||||||
//If difference is greater than a pennny, we need to adjust it.
|
//If difference is greater than a pennny, we need to adjust it.
|
||||||
|
|||||||
32
server/middleware/validateCanvasInputMiddleware.js
Normal file
32
server/middleware/validateCanvasInputMiddleware.js
Normal 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;
|
||||||
@@ -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 }));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
// });
|
||||||
|
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user