Compare commits

...

45 Commits

Author SHA1 Message Date
Allan Carr
646754732d IO-2782 Adjust to Object for items
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-20 16:45:48 -07:00
Dave Richer
efc1157653 IO-2782 - Fix query
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-20 18:53:33 -04:00
Dave Richer
a5d3f2caf1 IO-2782-Send-Promanager-Welcome-Email - Update for merge conflict
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-19 13:11:02 -04:00
Dave Richer
4ad87a522c IO-2782-Send-Promanager-Welcome-Email - Update for merge conflict
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-19 13:08:23 -04:00
Dave Richer
145cf7cc93 IO-2782-Send-Promanager-Welcome-Email - Finalize cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-19 12:54:26 -04:00
Dave Richer
cdb2d4d2d6 IO-2782-Send-Promanager-Welcome-Email - Cleanup of adminRoutes / firebase-handler.js
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-19 11:29:13 -04:00
Dave Richer
29f0031c1e IO-2782-Send-Promanager-Welcome-Email - Send ProManager welcome email
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-18 18:30:02 -04:00
Patrick Fic
e3059b41ae IO-2933 Resolve PR comments. 2024-09-18 11:19:43 -07:00
Patrick Fic
2a33f462a3 IO-2933 Add in email for succesful postback from Short URL. 2024-09-16 16:02:19 -07:00
Patrick Fic
cbc164dbeb IO-2933 Add in ability to text payments for multiple ROs. 2024-09-16 14:33:17 -07:00
Patrick Fic
6382fdf19c Merge branch 'feature/IO-2920-cash-discounting' into release/2024-09-20 2024-09-16 12:23:53 -07:00
Patrick Fic
9287e6608d IO-2920 Pretty JSON Translation 2024-09-16 12:23:29 -07:00
Patrick Fic
d221763064 Merge branch 'feature/IO-2920-cash-discounting' into release/2024-09-20 2024-09-16 12:13:13 -07:00
Patrick Fic
b39a5b755e IO-2920 Add hasura changes for cash discount & add config page. 2024-09-16 12:07:35 -07:00
Dave Richer
449330441a Merged in release/2024-09-13 (pull request #1725)
Release/2024 09 13 - IO-2913 - IO-2915 - IO-2733 - IO-2923 - IO-2925 - IO-2928 - IO-2927 - IO-2928 - IO-2913 - IO-2733 - IO-2926
2024-09-16 16:28:40 +00:00
Dave Richer
fcab5e6ef2 Merged in feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order (pull request #1723)
feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order - Fix Wrapping in Vendor Search Select
2024-09-16 16:18:05 +00:00
Dave Richer
0212b837ea feature/IO-2926-Vendor-Discount-Wrapping-In-Parts-Order - Fix Wrapping in Vendor Search Select
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-16 12:16:29 -04:00
Patrick Fic
e7438a099e Merged in feature/IO-2733-pwa-timer (pull request #1721)
IO-2733 Add loading state and further delay reload.
2024-09-13 18:24:11 +00:00
Patrick Fic
c69c86d193 Merged in feature/IO-2733-pwa-timer (pull request #1719)
IO-2733 Resolve notification showing incorrect time.
2024-09-13 17:51:33 +00:00
Patrick Fic
af09796df8 Merged in feature/IO-2733-pwa-timer (pull request #1717)
IO-2733 Add Timer Started check to prevent auto refresh early.
2024-09-13 16:59:58 +00:00
Allan Carr
0aba040338 Merged in feature/IO-2928-QBO-CAUSA-Payable-TAX (pull request #1714)
IO-2928 QBO CA US Tax  Accumulator

Approved-by: Patrick Fic
2024-09-13 14:43:58 +00:00
Allan Carr
c3bfe87674 Merge branch 'feature/IO-2913-ADP-Payroll' into release/2024-09-13
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	client/src/translations/en_us/common.json
#	client/src/translations/es/common.json
#	client/src/translations/fr/common.json
2024-09-12 19:59:47 -07:00
Allan Carr
9aa1279144 IO-2913 Add in Translations
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 19:53:50 -07:00
Allan Carr
4e6c45b195 IO-2928 Null coalesce billline amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 17:13:29 -07:00
Allan Carr
4fdb939bd2 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1715)
IO-2927 Extend Logging passthru

Approved-by: Patrick Fic
2024-09-12 23:58:24 +00:00
Allan Carr
062a1dcc72 IO-2927 Extend Logging passthru
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 16:29:37 -07:00
Allan Carr
7b420b1855 IO-2928 QBO CA US Tax Accumulator
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 16:27:43 -07:00
Allan Carr
40f61bbc8f Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1713)
IO-2927 Correct accountmeta
2024-09-12 22:23:54 +00:00
Allan Carr
f5d821c394 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1712)
IO-2927 Correct accountmeta
2024-09-12 22:12:32 +00:00
Allan Carr
3958ec9189 IO-2927 Correct accountmeta
accounts doesn't exist in recievables, switch to items

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-12 15:11:06 -07:00
Allan Carr
1e4f52e541 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1710)
Feature/IO-2927 qbo usa gst itc

IO-2927
2024-09-12 20:33:27 +00:00
Patrick Fic
5cc5cb444e Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1709)
IO-2997 Add better error handling for 400 requests.

Approved-by: Allan Carr
2024-09-12 20:26:16 +00:00
Patrick Fic
4acf0c59ca IO-2997 Remove unnecessary comment. 2024-09-12 13:25:14 -07:00
Patrick Fic
2858a5e871 IO-2997 Add better error handling for 400 requests. 2024-09-12 13:23:53 -07:00
Patrick Fic
24496d3ee1 Merged in feature/IO-2927-qbo-usa-gst-itc (pull request #1707)
IO-2927 Update QBO Payable to use ITC.

Approved-by: Allan Carr
2024-09-12 20:04:33 +00:00
Patrick Fic
0a5df69b12 IO-2927 Update QBO Payable to use ITC. 2024-09-12 13:03:23 -07:00
Patrick Fic
80efea02c6 Merged in feature/IO-2925-ppc-40-points (pull request #1704)
IO-2925 Add 40% as PPC choice.

Approved-by: Allan Carr
2024-09-12 17:15:03 +00:00
Patrick Fic
9f5c282b41 IO-2925 Add 40% as PPC choice. 2024-09-12 09:19:49 -07:00
Allan Carr
b2602c3385 Merged in feature/IO-2923-Edit-Bill-Line-original_actual_price (pull request #1702)
IO-2923 Edit Bill Line original_actual_price

Approved-by: Dave Richer
2024-09-12 15:54:58 +00:00
Allan Carr
0e584af424 IO-2923 Edit Bill Line original_actual_price
IO-2923 Edit Bill Line original_actual_price

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-11 16:36:28 -07:00
Patrick Fic
cdc3de2a33 Merge branch 'feature/IO-2733-pwa-timer' into release/2024-09-13 2024-09-10 15:58:59 -07:00
Allan Carr
44cb7577e2 Merged in feature/IO-2913-ADP-Payroll (pull request #1698)
IO-2913 ADP Payroll Reports

Approved-by: Dave Richer
2024-09-10 20:24:51 +00:00
Allan Carr
46d2b08477 Merged in feature/IO-2915-Customer-Portion-Totals-Federal-Tax (pull request #1699)
IO-2915 Customer Portion Totals - Federal Tax

Approved-by: Dave Richer
2024-09-10 20:24:13 +00:00
Allan Carr
0193ff9e65 IO-2915 Customer Portion Totals - Federal Tax
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-10 11:55:42 -07:00
Allan Carr
fd9a51209f IO-2913 ADP Payroll Reports
Uses ADPPayroll Split

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-09-10 11:21:03 -07:00
28 changed files with 8065 additions and 7341 deletions

View File

@@ -4730,6 +4730,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>batchid</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>bill_allow_post_to_closed</name>
<definition_loaded>false</definition_loaded>
@@ -4856,6 +4877,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>companycode</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>country</name>
<definition_loaded>false</definition_loaded>
@@ -5564,6 +5606,53 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>intellipay_config</name>
<children>
<concept_node>
<name>cash_discount_percentage</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>enable_cash_discount</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
<name>invoice_federal_tax_rate</name>
<definition_loaded>false</definition_loaded>
@@ -11109,6 +11198,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>intellipay</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>intellipay_cash_discount</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobstatuses</name>
<definition_loaded>false</definition_loaded>
@@ -22230,6 +22361,27 @@
<folder_node>
<name>buttons</name>
<children>
<concept_node>
<name>create_short_link</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>goback</name>
<definition_loaded>false</definition_loaded>
@@ -49635,6 +49787,48 @@
<folder_node>
<name>templates</name>
<children>
<concept_node>
<name>adp_payroll_flat</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>adp_payroll_straight</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>anticipated_revenue</name>
<definition_loaded>false</definition_loaded>

View File

@@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
});
billlines.forEach((billline) => {
const { deductedfromlbr, inventories, jobline, ...il } = billline;
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
delete il.__typename;
if (il.id) {

View File

@@ -1,6 +1,6 @@
import { DeleteFilled } from "@ant-design/icons";
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,10 +14,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import { getCurrentUser } from "../../firebase/firebase.utils";
const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment,
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
currentUser: getCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
@@ -25,11 +27,17 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
const CardPaymentModalComponent = ({
bodyshop,
currentUser,
cardPaymentModal,
toggleModalVisible,
insertAuditTrail
}) => {
const { context, actions } = cardPaymentModal;
const [form] = Form.useForm();
const [paymentLink, setPaymentLink] = useState();
const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
@@ -51,8 +59,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
//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.
setTimeout(() => {
if (actions && actions.refetch && typeof actions.refetch === "function")
actions.refetch();
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
@@ -86,7 +93,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
});
};
const handleIntelliPayCharge = async () => {
setLoading(true);
//Validate
@@ -101,7 +107,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue(),
paymentSplitMeta: form.getFieldsValue()
});
if (window.intellipay) {
@@ -126,6 +132,42 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
}
};
const handleIntelliPayChargeShortLink = async () => {
setLoading(true);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
try {
const { payments } = form.getFieldsValue();
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 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 })),
paymentSplitMeta: form.getFieldsValue()
});
if (response.data) {
setPaymentLink(response.data?.shorUrl);
navigator.clipboard.writeText(response.data?.shorUrl);
message.success(t("general.actions.copied"));
}
setLoading(false);
} catch (error) {
notification.open({
type: "error",
message: t("job_payments.notifications.error.openingip")
});
setLoading(false);
}
};
return (
<Card title="Card Payment">
<Spin spinning={loading}>
@@ -208,10 +250,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
{() => {
//If all of the job ids have been fileld in, then query and update the IP field.
const { payments } = form.getFieldsValue();
if (
payments?.length > 0 &&
payments?.filter((p) => p?.jobid).length === payments?.length
) {
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
refetch({ jobids: payments.map((p) => p.jobid) });
}
return (
@@ -246,7 +285,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
return (
<Space style={{ float: "right" }}>
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
@@ -273,11 +311,36 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
<Space direction="vertical" align="center">
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayChargeShortLink}
>
{t("job_payments.buttons.create_short_link")}
</Button>
</Space>
</Space>
);
}}
</Form.Item>
</Form>
{paymentLink && (
<Space
style={{ cursor: "pointer", float: "right" }}
align="end"
onClick={() => {
navigator.clipboard.writeText(paymentLink);
message.success(t("general.actions.copied"));
}}
>
<div>{paymentLink}</div>
<CopyFilled />
</Space>
)}
</Spin>
</Card>
);

View File

@@ -141,10 +141,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible
},
// {
// key: t("jobs.fields.federal_tax_payable"),
// total: job.job_totals.totals.custPayable.federal_tax,
// },
...(InstanceRenderManager({
imex: [{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}],
rome: [],
promanager: "USE_ROME"
})),
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount

View File

@@ -27,6 +27,10 @@ export default function PartsOrderModalPriceChange({ form, field }) {
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" })
},
{
key: "40",
label: t("parts_orders.labels.discount", { percent: "40%" })
},
{
key: "custom",
label: (

View File

@@ -8,11 +8,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
@@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(PaymentsGenerateLink);
export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone, setMessage }) {
export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, openChatByPhone, setMessage }) {
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -30,29 +31,35 @@ export function PaymentsGenerateLink({ bodyshop, callback, job, openChatByPhone,
const handleFinish = async ({ amount }) => {
setLoading(true);
const p = parsePhoneNumber(job.ownr_ph1, "CA");
let p;
try {
p = parsePhoneNumber(job.ownr_ph1 || "", "CA");
} catch (error) {
console.log("Unable to parse phone number");
}
setLoading(true);
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: amount,
account: job.ro_number,
invoice: job.id
comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email }))
});
setLoading(false);
setPaymentLink(response.data.shorUrl);
openChatByPhone({
phone_num: p.formatInternational(),
jobid: job.id
});
setMessage(
t("payments.labels.smspaymentreminder", {
shopname: bodyshop.shopname,
amount: amount,
payment_link: response.data.shorUrl
})
);
if (p) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: job.id
});
setMessage(
t("payments.labels.smspaymentreminder", {
shopname: bodyshop.shopname,
amount: amount,
payment_link: response.data.shorUrl
})
);
}
//Add in confirmation & errors.
if (callback) callback();

View File

@@ -34,28 +34,34 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
const {
treatments: { Enhanced_Payroll }
treatments: { Enhanced_Payroll, ADPPayroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
names: ["Enhanced_Payroll", "ADPPayroll"],
splitKey: bodyshop.imexshopid
});
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const Templates = TemplateList("report_center");
const ReportsList =
Enhanced_Payroll.treatment === "on"
? Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === true)
: Object.keys(Templates)
.map((key) => {
return Templates[key];
})
.filter((temp) => temp.enhanced_payroll === undefined || temp.enhanced_payroll === false);
const ReportsList = Object.keys(Templates)
.map((key) => Templates[key])
.filter((temp) => {
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
const adpPayrollOn = ADPPayroll.treatment === "on";
if (enhancedPayrollOn && adpPayrollOn) {
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
}
if (enhancedPayrollOn) {
return temp.enhanced_payroll !== false && temp.adp_payroll !== true;
}
if (adpPayrollOn) {
return temp.adp_payroll !== false && temp.enhanced_payroll !== true;
}
return temp.enhanced_payroll !== true && temp.adp_payroll !== true;
});
const { open } = reportCenterModal;
@@ -104,7 +110,7 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
to: values.to,
subject: Templates[values.key]?.subject
},
values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
values.sendbytext === "text" ? values.sendbytext : values.sendbyexcel === "excel" ? "x" : values.sendby === "email" ? "e" : "p",
id
);
setLoading(false);
@@ -291,7 +297,15 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
if (reporttype === "text")
return (
<Form.Item label={t("general.labels.sendby")} name="sendbytext" initialValue="text">
<Radio.Group>
<Radio value="text">{t("general.labels.text")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel" || reporttype !== "text")
return (
<Form.Item label={t("general.labels.sendby")} name="sendby" initialValue="print">
<Radio.Group>

View File

@@ -20,6 +20,7 @@ import ShopInfoTaskPresets from "./shop-info.task-presets.component";
import queryString from "query-string";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import ShopInfoRoGuard from "./shop-info.roguard.component";
import ShopInfoIntellipay from "./shop-intellipay-config.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -135,6 +136,17 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
],
rome: "USE_IMEX",
promanager: []
}),
...InstanceRenderManager({
imex: [],
rome: [
{
key: "intellipay",
label: t("bodyshop.labels.intellipay"),
children: <ShopInfoIntellipay form={form} />
}
],
promanager: []
})
];
return (

View File

@@ -7,13 +7,13 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
// TODO: Client Update, this might break
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({
@@ -28,10 +28,10 @@ export function ShopInfoGeneral({ form, bodyshop }) {
const { t } = useTranslation();
const {
treatments: { ClosingPeriod }
treatments: { ClosingPeriod, ADPPayroll }
} = useSplitTreatments({
attributes: {},
names: ["ClosingPeriod"],
names: ["ClosingPeriod", "ADPPayroll"],
splitKey: bodyshop && bodyshop.imexshopid
});
@@ -98,7 +98,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Form.Item label={t("bodyshop.fields.email")} name="email">
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.phone")}
name="phone"
@@ -356,14 +355,22 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Select mode="tags" />
</Form.Item>
{ClosingPeriod.treatment === "on" && (
<>
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
</>
<Form.Item
name={["accountingconfig", "ClosingPeriod"]}
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
>
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
<Input />
</Form.Item>
)}
{ADPPayroll.treatment === "on" && (
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
<Input />
</Form.Item>
)}
</LayoutFormRow>
</FeatureWrapper>

View File

@@ -0,0 +1,54 @@
import { Alert, Form, InputNumber, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
export function ShopInfoIntellipay({ bodyshop, form }) {
const { t } = useTranslation();
return (
<>
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
{() => {
const { intellipay_config } = form.getFieldsValue();
if (intellipay_config?.enable_cash_discount)
return <Alert message={t("bodyshop.labels.intellipay_cash_discount")} />;
}}
</Form.Item>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
valuePropName="checked"
name={["intellipay_config", "enable_cash_discount"]}
>
<Switch />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
valuePropName="checked"
dependencies={[["intellipay_config", "enable_cash_discount"]]}
name={["intellipay_config", "cash_discount_percentage"]}
rules={[
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
]}
>
<InputNumber min={0} max={100} precision={1} suffix='%'/>
</Form.Item>
</LayoutFormRow>
</>
);
}

View File

@@ -5,7 +5,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
const { Option } = Select;
//To be used as a form element only.
// To be used as a form element only.
const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => {
const [option, setOption] = useState(value);
@@ -33,9 +33,25 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
if (!value || !options) return label;
const discount = options?.find((o) => o.id === value)?.discount;
return (
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{label}</div>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{label}
</div>
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
</div>
);
@@ -45,36 +61,67 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
optionFilterProp="name"
onSelect={onSelect}
disabled={disabled || false}
optionLabelProp={"name"}
optionLabelProp="name"
>
{favorites
? favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row">
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
{favorites &&
favorites.map((o) => (
<Option key={`favorite-${o.id}`} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
</Option>
))
: null}
{options
? options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div className="imex-flex-row" style={{ width: "100%" }}>
<div style={{ flex: 1 }}>{o.name}</div>
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
<Space style={{ marginLeft: "1rem" }}>
<HeartOutlined style={{ color: "red" }} />
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
{options &&
options.map((o) => (
<Option key={o.id} value={o.id} name={o.name} discount={o.discount}>
<div
style={{
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
width: "100%"
}}
>
<div
style={{
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{o.name}
</div>
</Option>
))
: null}
<Space style={{ marginLeft: "1rem" }}>
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space>
</div>
</Option>
))}
</Select>
);
};

View File

@@ -138,7 +138,8 @@ export const QUERY_BODYSHOP = gql`
tt_enforce_hours_for_tech_console
md_tasks_presets
use_paint_scale_data
md_ro_guard
intellipay_config
md_ro_guard
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
id
name
@@ -266,7 +267,8 @@ export const UPDATE_SHOP = gql`
enforce_conversion_category
tt_enforce_hours_for_tech_console
md_tasks_presets
md_ro_guard
intellipay_config
md_ro_guard
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
id
name

View File

@@ -1,21 +1,21 @@
import { Tabs } from "antd";
import React, { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import queryString from "query-string";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -285,12 +285,14 @@
},
"appt_length": "",
"attach_pdf_to_email": "",
"batchid": "",
"bill_allow_post_to_closed": "",
"bill_federal_tax_rate": "",
"bill_local_tax_rate": "",
"bill_state_tax_rate": "",
"city": "",
"closingperiod": "",
"companycode": "",
"country": "",
"dailybodytarget": "",
"dailypainttarget": "",
@@ -330,6 +332,10 @@
"next_contact_hours": "",
"templates": ""
},
"intellipay_config": {
"cash_discount_percentage": "",
"enable_cash_discount": ""
},
"invoice_federal_tax_rate": "",
"invoice_local_tax_rate": "",
"invoice_state_tax_rate": "",
@@ -661,6 +667,8 @@
"filehandlers": "",
"insurancecos": "",
"intakechecklist": "",
"intellipay": "",
"intellipay_cash_discount": "",
"jobstatuses": "",
"laborrates": "",
"licensing": "",
@@ -1365,6 +1373,7 @@
},
"job_payments": {
"buttons": {
"create_short_link": "",
"goback": "",
"proceedtopayment": "",
"refundpayment": ""
@@ -2929,6 +2938,8 @@
"vendor": ""
},
"templates": {
"adp_payroll_flat": "",
"adp_payroll_straight": "",
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",

View File

@@ -2158,6 +2158,32 @@ export const TemplateList = (type, context) => {
field: i18n.t("tasks.fields.created_at")
},
group: "jobs"
},
adp_payroll_flat: {
title: i18n.t("reportcenter.templates.adp_payroll_flat"),
subject: i18n.t("reportcenter.templates.adp_payroll_flat"),
key: "adp_payroll_flat",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.committed_at")
},
group: "payroll",
adp_payroll: true
},
adp_payroll_straight: {
title: i18n.t("reportcenter.templates.adp_payroll_straight"),
subject: i18n.t("reportcenter.templates.adp_payroll_straight"),
key: "adp_payroll_straight",
reporttype: "text",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.timetickets"),
field: i18n.t("timetickets.fields.date")
},
group: "payroll",
adp_payroll: true
}
}
: {}),

View File

@@ -939,6 +939,7 @@
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- intellipay_config
- jc_hourly_rates
- jobsizelimit
- last_name_first
@@ -1040,6 +1041,7 @@
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- intellipay_config
- jc_hourly_rates
- last_name_first
- localmediaserverhttp
@@ -4240,6 +4242,63 @@
- active:
_eq: true
event_triggers:
- name: job_modified
definition:
enable_manual: false
update:
columns:
- clm_no
- v_make_desc
- date_next_contact
- status
- employee_csr
- employee_prep
- clm_total
- suspended
- employee_body
- ro_number
- actual_in
- ownr_co_nm
- v_model_yr
- comment
- job_totals
- v_vin
- ownr_fn
- scheduled_completion
- special_coverage_policy
- v_color
- ca_gst_registrant
- scheduled_delivery
- actual_delivery
- actual_completion
- kanbanparent
- est_ct_fn
- employee_refinish
- ownr_ph1
- date_last_contacted
- alt_transport
- inproduction
- est_ct_ln
- production_vars
- category
- v_model_desc
- date_invoiced
- est_co_nm
- ownr_ln
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/job/job-updated'
version: 2
- name: job_status_transition
definition:
enable_manual: true

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "intellipay_config" jsonb
-- not null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "intellipay_config" jsonb
not null default jsonb_build_object();

View File

@@ -94,7 +94,10 @@ exports.default = async (req, res) => {
ret.push({
billid: bill.id,
success: false,
errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message)
errorMessage:
(error && error.authResponse && error.authResponse.body) ||
error.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
(error && error.message)
});
//Add the export log error.
@@ -209,14 +212,14 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
AccountBasedExpenseLineDetail: {
...(bill.job.class ? { ClassRef: { value: classes[bill.job.class] } } : {}),
AccountRef: {
value: accounts[bodyshop.md_responsibility_centers.taxes.federal.accountdesc]
value: accounts[bodyshop.md_responsibility_centers.taxes.federal_itc.accountdesc]
}
},
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
return acc + val.actual_cost * val.quantity;
return acc + val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0;
}, 0) * 100
)
})
@@ -274,6 +277,8 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
} catch (error) {
logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, {
error: error, //(error && error.authResponse && error.authResponse.body) || (error && error.message),
validationError: JSON.stringify(error?.response?.data),
accountmeta: JSON.stringify({ accounts, taxCodes, classes }),
method: "InsertBill"
});
throw error;

View File

@@ -179,7 +179,11 @@ exports.default = async (req, res) => {
ret.push({
jobid: job.id,
success: false,
errorMessage: (error && error.authResponse && error.authResponse.body) || (error && error.message)
errorMessage:
error?.authResponse?.body ||
error?.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
error?.response?.data ||
error?.message
});
console.log(error);
logger.log("qbo-receivable-create-error", "ERROR", req.user.email, {
@@ -254,7 +258,6 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
throw new Error(
`Insurance Company '${job.ins_co_nm}' not found in shop configuration. Please make sure it exists or change the insurance company name on the job to one that exists.`
);
return;
}
const Customer = {
DisplayName: job.ins_co_nm.trim(),
@@ -575,7 +578,9 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner"
method: "InsertInvoice",
validationError: JSON.stringify(error?.response?.data),
accountmeta: JSON.stringify({ items, taxCodes, classes })
});
throw error;
}

View File

@@ -96,7 +96,21 @@ const sendServerEmail = async ({ subject, text }) => {
}
};
const sendTaskEmail = async ({ to, subject, text, attachments }) => {
const sendProManagerWelcomeEmail = async ({to, subject, html}) => {
try {
await transporter.sendMail({
from: `ProManager <noreply@promanager.web-est.com>`,
to,
subject,
html
});
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
}
};
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
try {
transporter.sendMail(
{
@@ -107,7 +121,7 @@ const sendTaskEmail = async ({ to, subject, text, attachments }) => {
}),
to: to,
subject: subject,
text: text,
...(type === "text" ? { text } : { html }),
attachments: attachments || null
},
(err, info) => {
@@ -309,5 +323,6 @@ module.exports = {
sendEmail,
sendServerEmail,
sendTaskEmail,
sendProManagerWelcomeEmail,
emailBounce
};

View File

@@ -94,8 +94,9 @@ const formatPriority = (priority) => {
* @param taskId
* @returns {{header, body: string, subHeader: string}}
*/
const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId) => {
const endPoints = InstanceManager({
const getEndpoints = () =>
InstanceManager({
imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online",
rome:
bodyshop.convenient_company === "promanager"
@@ -106,6 +107,9 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j
? "https//test.romeonline.io"
: "https://romeonline.io"
});
const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId) => {
const endPoints = getEndpoints();
return {
header: title,
subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)}`,
@@ -333,5 +337,6 @@ const tasksRemindEmail = async (req, res) => {
module.exports = {
taskAssignedEmail,
tasksRemindEmail
tasksRemindEmail,
getEndpoints
};

View File

@@ -1,30 +1,28 @@
const admin = require("firebase-admin");
const logger = require("../utils/logger");
const path = require("path");
const { auth } = require("firebase-admin");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const client = require("../graphql-client/graphql-client").client;
const admin = require("firebase-admin");
const logger = require("../utils/logger");
const { sendProManagerWelcomeEmail } = require("../email/sendemail");
const client = require("../graphql-client/graphql-client").client;
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
const adminEmail = require("../utils/adminEmail");
const generateEmailTemplate = require("../email/generateTemplate");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.FIREBASE_DATABASE_URL
});
exports.admin = admin;
exports.createUser = async (req, res) => {
const createUser = async (req, res) => {
logger.log("admin-create-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true
});
const { email, displayName, password, shopid, authlevel } = req.body;
const { email, displayName, password, shopid, authlevel, validemail } = req.body;
try {
const userRecord = await admin.auth().createUser({ email, displayName, password });
@@ -42,6 +40,7 @@ exports.createUser = async (req, res) => {
user: {
email: email.toLowerCase(),
authid: userRecord.uid,
validemail,
associations: {
data: [{ shopid, authlevel, active: true }]
}
@@ -58,21 +57,115 @@ exports.createUser = async (req, res) => {
}
};
exports.updateUser = (req, res) => {
const sendPromanagerWelcomeEmail = (req, res) => {
const { authid, email } = req.body;
// Fetch user from Firebase
admin
.auth()
.getUser(authid)
.then((userRecord) => {
if (!userRecord) {
return Promise.reject({ status: 404, message: "User not found in Firebase." });
}
// Fetch user data from the database using GraphQL
return client.request(
`
query GET_USER_BY_EMAIL($email: String!) {
users(where: { email: { _eq: $email } }) {
email
validemail
associations {
id
shopid
bodyshop {
id
convenient_company
}
}
}
}`,
{ email: email.toLowerCase() }
);
})
.then((dbUserResult) => {
const dbUser = dbUserResult?.users?.[0];
if (!dbUser) {
return Promise.reject({ status: 404, message: "User not found in database." });
}
// Validate email before proceeding
if (!dbUser.validemail) {
logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, {
message: "User email is not valid, skipping email.",
email
});
return res.status(200).json({ message: "User email is not valid, email not sent." });
}
// Check if the user's company is ProManager
const convenientCompany = dbUser.associations?.[0]?.bodyshop?.convenient_company;
if (convenientCompany !== "promanager") {
logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, {
message: 'convenient_company is not "promanager", skipping email.',
convenientCompany
});
return res.status(200).json({ message: `convenient_company is not "promanager", email not sent.` });
}
// Generate password reset link
return admin
.auth()
.generatePasswordResetLink(dbUser.email)
.then((resetLink) => ({ dbUser, resetLink }));
})
.then(({ dbUser, resetLink }) => {
// Send welcome email (replace with your actual email-sending service)
return sendProManagerWelcomeEmail({
to: dbUser.email,
subject: "Welcome to the ProManager platform.",
html: generateEmailTemplate({
header: "",
subHeader: "",
body: `
<p>Welcome to the ProManager platform. Please click the link below to reset your password:</p>
<p><a href="${resetLink}">Reset your password</a></p>
<p>User Details:</p>
<ul>
<li>Email: ${dbUser.email}</li>
</ul>
`
})
});
})
.then(() => {
// Log success and return response
logger.log("admin-send-welcome-email", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true,
emailSentTo: email
});
res.status(200).json({ message: "Welcome email sent successfully." });
})
.catch((error) => {
logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { error });
if (!res.headersSent) {
res.status(error.status || 500).json({
message: error.message || "Error sending welcome email.",
error
});
}
});
};
const updateUser = (req, res) => {
logger.log("admin-update-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, {
request: req.body,
user: req.user
});
res.sendStatus(404);
return;
}
admin
.auth()
.updateUser(
@@ -105,26 +198,45 @@ exports.updateUser = (req, res) => {
});
};
exports.getUser = (req, res) => {
const getUser = (req, res) => {
logger.log("admin-get-user", "ADMIN", req.user.email, null, {
request: req.body,
ioadmin: true
});
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, {
request: req.body,
user: req.user
});
res.sendStatus(404);
return;
}
admin
.auth()
.getUser(req.body.uid)
.then((userRecord) => {
res.json(userRecord);
return client
.request(
`
query GET_USER_BY_AUTHID($authid: String!) {
users(where: { authid: { _eq: $authid } }) {
email
validemail
associations {
id
shopid
bodyshop {
id
convenient_company
}
}
}
}
`,
{ authid: req.body.uid }
)
.then((dbUserResult) => {
res.json({
...userRecord,
db: {
validemail: dbUserResult?.users?.[0]?.validemail,
company: dbUserResult?.users?.[0]?.associations?.[0]?.bodyshop?.convenient_company
}
});
});
})
.catch((error) => {
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
@@ -134,7 +246,7 @@ exports.getUser = (req, res) => {
});
};
exports.sendNotification = async (req, res) => {
const sendNotification = async (req, res) => {
setTimeout(() => {
// Send a message to the device corresponding to the provided
// registration token.
@@ -167,7 +279,7 @@ exports.sendNotification = async (req, res) => {
}, 500);
};
exports.subscribe = async (req, res) => {
const subscribe = async (req, res) => {
const result = await admin
.messaging()
.subscribeToTopic(req.body.fcm_tokens, `${req.body.imexshopid}-${req.body.type}`);
@@ -175,7 +287,7 @@ exports.subscribe = async (req, res) => {
res.json(result);
};
exports.unsubscribe = async (req, res) => {
const unsubscribe = async (req, res) => {
try {
const result = await admin
.messaging()
@@ -187,6 +299,17 @@ exports.unsubscribe = async (req, res) => {
}
};
module.exports = {
admin,
createUser,
updateUser,
getUser,
sendPromanagerWelcomeEmail,
sendNotification,
subscribe,
unsubscribe
};
//Admin claims code.
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";

View File

@@ -2502,6 +2502,13 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) {
jobs(where: {id: {_in: $ids}}) {
id
shopid
ro_number
ownr_co_nm
ownr_fn
ownr_ln
v_make_desc
v_model_yr
v_model_desc
}
}
`;

View File

@@ -7,7 +7,9 @@ const axios = require("axios");
const moment = require("moment");
const logger = require("../utils/logger");
const InstanceManager = require("../utils/instanceMgr").default;
const { sendTaskEmail } = require("../email/sendemail");
const generateEmailTemplate = require("../email/generateTemplate");
const { getEndpoints } = require("../email/tasksEmails");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
@@ -129,6 +131,7 @@ exports.generate_payment_url = async (req, res) => {
//...req.body,
amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"),
account: req.body.account,
comment: req.body.comment,
invoice: req.body.invoice,
createshorturl: true
//The postback URL is set at the CP teller global terminal settings page.
@@ -162,7 +165,67 @@ exports.postback = async (req, res) => {
return;
}
if (values.invoice) {
if (comment) {
//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.
const parsedComment = JSON.parse(comment);
//Adding in the user email to the short pay email.
//Need to check this to ensure backwards compatibility for clients that don't update.
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
ids: partialPayments.map((p) => p.jobid)
});
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
paymentInput: partialPayments.map((p) => ({
amount: p.amount,
transactionid: values.authcode,
payer: "Customer",
type: values.cardtype,
jobid: p.jobid,
date: moment(Date.now()),
payment_responses: {
data: {
amount: values.total,
bodyshopid: jobs.jobs[0].shopid,
jobid: p.jobid,
declinereason: "Approved",
ext_paymentid: values.paymentid,
successful: true,
response: values
}
}
}))
});
logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, null, {
iprequest: values,
paymentResult
});
if (values.origin === "OneLink" && parsedComment.userEmail) {
//Send an email, it was a text to pay link.
const endPoints = getEndpoints();
sendTaskEmail({
to: parsedComment.userEmail,
subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`,
type: "html",
html: generateEmailTemplate({
header: "New Payment(s) Received",
subHeader: "",
body: jobs.jobs
.map(
(job) =>
`Reference: <a href="${endPoints}/manage/jobs/${job.id}">${job.ro_number || "N/A"}</a> | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}`
)
.join("<br/>")
})
});
res.sendStatus(200);
}
} else if (values.invoice) {
//This is a link email that's been sent out.
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
id: values.invoice
@@ -198,39 +261,6 @@ exports.postback = async (req, res) => {
paymentResult
});
res.sendStatus(200);
} else if (comment) {
//This has been triggered by IO and may have multiple jobs.
const partialPayments = JSON.parse(comment);
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
ids: partialPayments.map((p) => p.jobid)
});
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
paymentInput: partialPayments.map((p) => ({
amount: p.amount,
transactionid: values.authcode,
payer: "Customer",
type: values.cardtype,
jobid: p.jobid,
date: moment(Date.now()),
payment_responses: {
data: {
amount: values.total,
bodyshopid: jobs.jobs[0].shopid,
jobid: p.jobid,
declinereason: "Approved",
ext_paymentid: values.paymentid,
successful: true,
response: values
}
}
}))
});
logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, null, {
iprequest: values,
paymentResult
});
res.sendStatus(200);
}
} catch (error) {
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {

View File

@@ -1,18 +1,20 @@
const express = require("express");
const router = express.Router();
const fb = require("../firebase/firebase-handler");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { createAssociation, createShop, updateShop, updateCounter } = require("../admin/adminops");
const { updateUser, getUser, createUser, sendPromanagerWelcomeEmail } = require("../firebase/firebase-handler");
const validateAdminMiddleware = require("../middleware/validateAdminMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.use(validateAdminMiddleware);
router.post("/createassociation", validateAdminMiddleware, createAssociation);
router.post("/createshop", validateAdminMiddleware, createShop);
router.post("/updateshop", validateAdminMiddleware, updateShop);
router.post("/updatecounter", validateAdminMiddleware, updateCounter);
router.post("/updateuser", fb.updateUser);
router.post("/getuser", fb.getUser);
router.post("/createuser", fb.createUser);
router.post("/createassociation", createAssociation);
router.post("/createshop", createShop);
router.post("/updateshop", updateShop);
router.post("/updatecounter", updateCounter);
router.post("/updateuser", updateUser);
router.post("/getuser", getUser);
router.post("/createuser", createUser);
router.post("/promanagerwelcome", sendPromanagerWelcomeEmail);
module.exports = router;