Compare commits

..

8 Commits

Author SHA1 Message Date
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
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
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
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
11 changed files with 10814 additions and 12912 deletions

View File

@@ -226,9 +226,7 @@ jobs:
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 5
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
sleep 10
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (Rome) - Hasura
@@ -315,9 +313,7 @@ jobs:
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 15
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
sleep 30
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (ImEX) - Hasura
@@ -427,7 +423,7 @@ workflows:
secret: ${HASURA_PROD_SECRET}
filters:
branches:
only: master-AIO
only: master
- rome-api-deploy:
filters:
branches:

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>

View File

@@ -1,14 +1,13 @@
import { AlertOutlined } from "@ant-design/icons";
import { Alert, Button, Col, notification, Row, Space } from "antd";
import { Alert, Button, Col, Row, Space } from "antd";
import i18n from "i18next";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectUpdateAvailable } from "../../redux/application/application.selectors";
import { useRegisterSW } from "virtual:pwa-register/react";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import useCountDown from "../../utils/countdownHook";
const mapStateToProps = createStructuredSelector({
updateAvailable: selectUpdateAvailable
@@ -20,15 +19,6 @@ const mapDispatchToProps = (dispatch) => ({
export function UpdateAlert({ updateAvailable }) {
const { t } = useTranslation();
const [timerStarted, setTimerStarted] = useState(false);
const [loading, setLoading] = useState(false);
const [
timeLeft,
{
start //pause, resume, reset
}
] = useCountDown(180000, 1000);
const {
offlineReady: [offlineReady],
needRefresh: [needRefresh],
@@ -50,43 +40,11 @@ export function UpdateAlert({ updateAvailable }) {
}
});
const ReloadNewVersion = useCallback(() => {
setLoading(true);
updateServiceWorker(true);
setTimeout(() => {
window.location.reload(true);
}, 5000);
}, [updateServiceWorker]);
useEffect(() => {
if (needRefresh) {
start();
setTimerStarted(true);
if (import.meta.env.DEV) {
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
}
}, [start, needRefresh, offlineReady]);
useEffect(() => {
if (needRefresh && timerStarted && timeLeft < 60000) {
notification.open({
type: "warning",
closable: false,
duration: 65000,
key: "autoupdate",
message: t("general.actions.autoupdate", {
time: (timeLeft / 1000).toFixed(0),
app: InstanceRenderManager({
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)",
promanager: "$t(titles.promanager)"
})
}),
placement: "bottomRight"
});
}
if (needRefresh && timerStarted && timeLeft <= 0) {
ReloadNewVersion();
}
}, [timeLeft, t, needRefresh, ReloadNewVersion, timerStarted]);
}, [needRefresh, offlineReady]);
if (!needRefresh) return null;
@@ -117,10 +75,9 @@ export function UpdateAlert({ updateAvailable }) {
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button loading={loading} type="primary" onClick={() => ReloadNewVersion()}>
{i18n.t("general.actions.refresh")} {`(${(timeLeft / 1000).toFixed(0)} s)`}
<Button type="primary" onClick={() => updateServiceWorker(true)}>
{i18n.t("general.actions.refresh")}
</Button>
<Button onClick={() => start(300000)}>{i18n.t("general.actions.delay")}</Button>
</Space>
</Col>
</Row>

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
import React from "react";
const useCountDown = (timeToCount = 60 * 1000, interval = 1000) => {
const [timeLeft, setTimeLeft] = React.useState(0);
const timer = React.useRef({});
const run = (ts) => {
if (!timer.current.started) {
timer.current.started = ts;
timer.current.lastInterval = ts;
}
const localInterval = Math.min(interval, timer.current.timeLeft || Infinity);
if (ts - timer.current.lastInterval >= localInterval) {
timer.current.lastInterval += localInterval;
setTimeLeft((timeLeft) => {
timer.current.timeLeft = timeLeft - localInterval;
return timer.current.timeLeft;
});
}
if (ts - timer.current.started < timer.current.timeToCount) {
timer.current.requestId = window.requestAnimationFrame(run);
} else {
timer.current = {};
setTimeLeft(0);
}
};
const start = React.useCallback(
(ttc) => {
window.cancelAnimationFrame(timer.current.requestId);
const newTimeToCount = ttc !== undefined ? ttc : timeToCount;
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = newTimeToCount;
timer.current.requestId = window.requestAnimationFrame(run);
setTimeLeft(newTimeToCount);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const pause = React.useCallback(() => {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.started = null;
timer.current.lastInterval = null;
timer.current.timeToCount = timer.current.timeLeft;
}, []);
const resume = React.useCallback(
() => {
if (!timer.current.started && timer.current.timeLeft > 0) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current.requestId = window.requestAnimationFrame(run);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const reset = React.useCallback(() => {
if (timer.current.timeLeft) {
window.cancelAnimationFrame(timer.current.requestId);
timer.current = {};
setTimeLeft(0);
}
}, []);
const actions = React.useMemo(
() => ({ start, pause, resume, reset }), // eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
React.useEffect(() => {
return () => window.cancelAnimationFrame(timer.current.requestId);
}, []);
return [timeLeft, actions];
};
export default useCountDown;

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,7 +212,7 @@ 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]
}
},
@@ -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,10 @@ exports.default = async (req, res) => {
ret.push({
jobid: job.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)
});
console.log(error);
logger.log("qbo-receivable-create-error", "ERROR", req.user.email, {
@@ -254,7 +257,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 +577,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;
}