Compare commits
56 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
665f09d832 | ||
|
|
3d7f2961fd | ||
|
|
af52c35013 | ||
|
|
36157d87bb | ||
|
|
722375fede | ||
|
|
339c19a041 | ||
|
|
8af8c8039c | ||
|
|
b8570f3ae9 | ||
|
|
6ef56f97c0 | ||
|
|
3a1d10b0d1 | ||
|
|
dd633cea89 | ||
|
|
e6071709be | ||
|
|
fb863c7979 | ||
|
|
c95c11fd0e | ||
|
|
1351fbb814 | ||
|
|
dcd3a078ef | ||
|
|
bb8e140f6e | ||
|
|
8102fd5177 | ||
|
|
bf11e10676 | ||
|
|
92e6bdf2a2 | ||
|
|
a02e336d73 | ||
|
|
7ec8a73c30 | ||
|
|
c7bb1a9c32 | ||
|
|
e669c19b98 | ||
|
|
5c55c0c74b | ||
|
|
f1f705903a | ||
|
|
6551be2d92 | ||
|
|
48e59fe849 | ||
|
|
7991192496 | ||
|
|
05cd60c2a1 | ||
|
|
26fc76a767 | ||
|
|
49816d5d43 | ||
|
|
b9b3e2c2aa | ||
|
|
e3c02f94f1 | ||
|
|
490dd662d5 | ||
|
|
8d00fc29d1 | ||
|
|
f04f48f593 | ||
|
|
721e9bc464 | ||
|
|
76c828a1c9 | ||
|
|
7e5363f911 | ||
|
|
f5b16394f9 | ||
|
|
7132465945 | ||
|
|
a873a2573a | ||
|
|
ff24db6561 | ||
|
|
da26954c3b | ||
|
|
6991cf60e5 | ||
|
|
818aedf04f | ||
|
|
1cb6834207 | ||
|
|
8577929bd4 | ||
|
|
f44121e06b | ||
|
|
faf9fb75c5 | ||
|
|
97d8047a3d | ||
|
|
16220d0a27 | ||
|
|
51fba24a3d | ||
|
|
52f43a600c | ||
|
|
e25174ff97 |
3716
client/package-lock.json
generated
3716
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,59 +2,60 @@
|
|||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/analytics-browser": "^2.35.3",
|
"@amplitude/analytics-browser": "^2.36.5",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.6",
|
"@apollo/client": "^4.1.6",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@documenso/embed-react": "^0.6.0",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
||||||
"@firebase/analytics": "^0.10.19",
|
"@firebase/analytics": "^0.10.20",
|
||||||
"@firebase/app": "^0.14.8",
|
"@firebase/app": "^0.14.9",
|
||||||
"@firebase/auth": "^1.12.0",
|
"@firebase/auth": "^1.12.1",
|
||||||
"@firebase/firestore": "^4.11.0",
|
"@firebase/firestore": "^4.12.0",
|
||||||
"@firebase/messaging": "^0.12.22",
|
"@firebase/messaging": "^0.12.24",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@sentry/cli": "^3.2.2",
|
"@sentry/cli": "^3.3.3",
|
||||||
"@sentry/react": "^10.40.0",
|
"@sentry/react": "^10.43.0",
|
||||||
"@sentry/vite-plugin": "^4.9.1",
|
"@sentry/vite-plugin": "^4.9.1",
|
||||||
"@splitsoftware/splitio-react": "^2.6.1",
|
"@splitsoftware/splitio-react": "^2.6.1",
|
||||||
"@tanem/react-nprogress": "^5.0.63",
|
"@tanem/react-nprogress": "^5.0.63",
|
||||||
"antd": "^6.3.1",
|
"antd": "^6.3.3",
|
||||||
"apollo-link-logger": "^3.0.0",
|
"apollo-link-logger": "^3.0.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.6",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.20",
|
||||||
"dayjs-business-days2": "^1.3.2",
|
"dayjs-business-days2": "^1.3.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"env-cmd": "^11.0.0",
|
"env-cmd": "^11.0.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.13.0",
|
"graphql": "^16.13.1",
|
||||||
"graphql-ws": "^6.0.7",
|
"graphql-ws": "^6.0.7",
|
||||||
"i18next": "^25.8.13",
|
"i18next": "^25.8.18",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.38",
|
"libphonenumber-js": "^1.12.40",
|
||||||
"lightningcss": "^1.31.1",
|
"lightningcss": "^1.32.0",
|
||||||
"logrocket": "^12.0.0",
|
"logrocket": "^12.1.0",
|
||||||
"markerjs2": "^2.32.7",
|
"markerjs2": "^2.32.7",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.1.1",
|
"normalize-url": "^8.1.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"posthog-js": "^1.355.0",
|
"posthog-js": "^1.360.2",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
@@ -65,8 +66,8 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-i18next": "^16.5.4",
|
"react-i18next": "^16.5.8",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
@@ -76,8 +77,8 @@
|
|||||||
"react-resizable": "^3.1.3",
|
"react-resizable": "^3.1.3",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.18.1",
|
"react-virtuoso": "^4.18.3",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.8.0",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.98.0",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"styled-components": "^6.3.11",
|
"styled-components": "^6.3.11",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
@@ -140,15 +141,16 @@
|
|||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@dotenvx/dotenvx": "^1.52.0",
|
"@dotenvx/dotenvx": "^1.55.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@rolldown/plugin-babel": "^0.2.1",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"browserslist": "^4.28.1",
|
"browserslist": "^4.28.1",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
@@ -156,21 +158,18 @@
|
|||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.4.0",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"memfs": "^4.56.10",
|
"memfs": "^4.56.11",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0",
|
||||||
"vite-plugin-babel": "^1.5.1",
|
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vitest": "^4.1.0",
|
||||||
"vitest": "^4.0.18",
|
|
||||||
"workbox-window": "^7.4.0"
|
"workbox-window": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,3 +509,10 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.esignature-embed {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
const emailsToMenu = {
|
const emailsToMenu = {
|
||||||
items: [
|
items: [
|
||||||
...bodyshop.employees
|
...bodyshop.employees
|
||||||
.filter((e) => e.user_email)
|
.filter((e) => e.user_email && e.active === true)
|
||||||
.map((e, idx) => ({
|
.map((e, idx) => ({
|
||||||
key: idx,
|
key: idx,
|
||||||
label: `${e.first_name} ${e.last_name}`,
|
label: `${e.first_name} ${e.last_name}`,
|
||||||
@@ -59,7 +59,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b
|
|||||||
const menuCC = {
|
const menuCC = {
|
||||||
items: [
|
items: [
|
||||||
...bodyshop.employees
|
...bodyshop.employees
|
||||||
.filter((e) => e.user_email)
|
.filter((e) => e.user_email && e.active === true)
|
||||||
.map((e, idx) => ({
|
.map((e, idx) => ({
|
||||||
key: idx,
|
key: idx,
|
||||||
label: `${e.first_name} ${e.last_name}`,
|
label: `${e.first_name} ${e.last_name}`,
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
||||||
|
import { Modal } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectEsignature } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
esignatureModal: selectEsignature,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("esignature"))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { open, context } = esignatureModal;
|
||||||
|
const { token, envelopeId, documentId, jobid } = context;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title={t("jobs.labels.esignature")}
|
||||||
|
onOk={async () => {
|
||||||
|
try {
|
||||||
|
const distResult = await axios.post("/esign/distribute", {
|
||||||
|
documentId,
|
||||||
|
envelopeId,
|
||||||
|
jobid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
console.log("Distribution result:", distResult);
|
||||||
|
toggleModalVisible();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error distributing document:", error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={async () => {
|
||||||
|
try {
|
||||||
|
const cancelResult = await axios.post("/esign/delete", {
|
||||||
|
documentId,
|
||||||
|
envelopeId
|
||||||
|
});
|
||||||
|
console.log("Cancel result:", cancelResult);
|
||||||
|
toggleModalVisible();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error cancelling document:", error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
okButtonProps={{ title: "Distribute by Email" }}
|
||||||
|
width="90%"
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<div style={{ height: "600px", width: "100%" }}>
|
||||||
|
{token ? (
|
||||||
|
<EmbedUpdateDocumentV1
|
||||||
|
presignToken={token}
|
||||||
|
host="https://stg-app.documenso.com"
|
||||||
|
documentId={documentId}
|
||||||
|
className="esignature-embed"
|
||||||
|
onDocumentUpdated={(data) => {
|
||||||
|
console.log("Document updated:", data.documentId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>No token...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EsignatureModalContainer);
|
||||||
@@ -25,6 +25,7 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
|||||||
|
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (!e.target) return;
|
||||||
const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight;
|
const bottom = e.target.scrollHeight - 100 <= e.target.scrollTop + e.target.clientHeight;
|
||||||
if (bottom && !hasEverScrolledToBottom) {
|
if (bottom && !hasEverScrolledToBottom) {
|
||||||
setHasEverScrolledToBottom(true);
|
setHasEverScrolledToBottom(true);
|
||||||
@@ -36,7 +37,9 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleScroll({ target: markdownCardRef.current });
|
if (markdownCardRef.current) {
|
||||||
|
handleScroll({ target: markdownCardRef.current });
|
||||||
|
}
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||||
if (!value) return null;
|
if (value === null || value === undefined || value === "") return null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "employee": {
|
case "employee": {
|
||||||
const emp = bodyshop.employees.find((e) => e.id === value);
|
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { FaTasks } from "react-icons/fa";
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
@@ -49,6 +49,7 @@ import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
|||||||
import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component";
|
import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component.jsx";
|
||||||
|
|
||||||
const UPDATE_JOB_LINES_LOCATION_BULK = gql`
|
const UPDATE_JOB_LINES_LOCATION_BULK = gql`
|
||||||
mutation UPDATE_JOB_LINES_LOCATION_BULK($ids: [uuid!]!, $location: String!) {
|
mutation UPDATE_JOB_LINES_LOCATION_BULK($ids: [uuid!]!, $location: String!) {
|
||||||
@@ -66,7 +67,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
technician: selectTechnician,
|
technician: selectTechnician,
|
||||||
isPartsEntry: selectIsPartsEntry
|
isPartsEntry: selectIsPartsEntry,
|
||||||
|
authLevel: selectAuthLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -94,7 +96,8 @@ export function JobLinesComponent({
|
|||||||
setTaskUpsertContext,
|
setTaskUpsertContext,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handlePartsOrderOnRowClick,
|
handlePartsOrderOnRowClick,
|
||||||
isPartsEntry
|
isPartsEntry,
|
||||||
|
authLevel
|
||||||
}) {
|
}) {
|
||||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||||
const [bulkUpdateLocations] = useMutation(UPDATE_JOB_LINES_LOCATION_BULK);
|
const [bulkUpdateLocations] = useMutation(UPDATE_JOB_LINES_LOCATION_BULK);
|
||||||
@@ -386,18 +389,20 @@ export function JobLinesComponent({
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
{(record.manual_line || jobIsPrivate) &&
|
||||||
<Button
|
!technician &&
|
||||||
disabled={jobRO}
|
HasRbacAccess({ bodyshop, authLevel, action: "jobs:manual-line" }) && (
|
||||||
onClick={() => {
|
<Button
|
||||||
setJobLineEditContext({
|
disabled={jobRO}
|
||||||
actions: { refetch: refetch, submit: form && form.submit },
|
onClick={() => {
|
||||||
context: { ...record, jobid: job.id }
|
setJobLineEditContext({
|
||||||
});
|
actions: { refetch: refetch, submit: form && form.submit },
|
||||||
}}
|
context: { ...record, jobid: job.id }
|
||||||
icon={<EditFilled />}
|
});
|
||||||
/>
|
}}
|
||||||
)}
|
icon={<EditFilled />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.create")}
|
title={t("tasks.buttons.create")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -410,29 +415,30 @@ export function JobLinesComponent({
|
|||||||
}}
|
}}
|
||||||
icon={<FaTasks />}
|
icon={<FaTasks />}
|
||||||
/>
|
/>
|
||||||
|
{(record.manual_line || jobIsPrivate) &&
|
||||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
!technician &&
|
||||||
<Button
|
HasRbacAccess({ bodyshop, authLevel, action: "jobs:manual-line" }) && (
|
||||||
disabled={jobRO}
|
<Button
|
||||||
onClick={async () => {
|
disabled={jobRO}
|
||||||
await deleteJobLine({
|
onClick={async () => {
|
||||||
variables: { joblineId: record.id },
|
await deleteJobLine({
|
||||||
update(cache) {
|
variables: { joblineId: record.id },
|
||||||
cache.modify({
|
update(cache) {
|
||||||
fields: {
|
cache.modify({
|
||||||
joblines(existingJobLines, { readField }) {
|
fields: {
|
||||||
return existingJobLines.filter((jlRef) => record.id !== readField("id", jlRef));
|
joblines(existingJobLines, { readField }) {
|
||||||
|
return existingJobLines.filter((jlRef) => record.id !== readField("id", jlRef));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
});
|
await axios.post("/job/totalsssu", { id: job.id });
|
||||||
await axios.post("/job/totalsssu", { id: job.id });
|
if (refetch) refetch();
|
||||||
if (refetch) refetch();
|
}}
|
||||||
}}
|
icon={<DeleteFilled />}
|
||||||
icon={<DeleteFilled />}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -657,7 +663,7 @@ export function JobLinesComponent({
|
|||||||
<Button id="repair-data-mark-button">{t("jobs.actions.mark")}</Button>
|
<Button id="repair-data-mark-button">{t("jobs.actions.mark")}</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
||||||
{!isPartsEntry && (
|
{!isPartsEntry && HasRbacAccess({ bodyshop, authLevel, action: "jobs:manual-line" }) && (
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO || technician}
|
disabled={jobRO || technician}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -144,18 +144,11 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
{t("jobs.labels.mapa")}
|
{t("jobs.labels.mapa")}
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex:
|
imex:
|
||||||
job.materials?.mapa &&
|
(job.materials?.mapa ?? job.materials?.MAPA)?.cal_maxdlr > 0 &&
|
||||||
job.materials.mapa.cal_maxdlr &&
|
t("jobs.labels.threshhold", { amount: (job.materials.mapa ?? job.materials.MAPA).cal_maxdlr }),
|
||||||
job.materials.mapa.cal_maxdlr > 0 &&
|
|
||||||
t("jobs.labels.threshhold", {
|
|
||||||
amount: job.materials.mapa.cal_maxdlr
|
|
||||||
}),
|
|
||||||
rome:
|
rome:
|
||||||
job.materials?.MAPA &&
|
job.materials?.MAPA?.cal_maxdlr !== undefined &&
|
||||||
job.materials.MAPA.cal_maxdlr !== undefined &&
|
t("jobs.labels.threshhold", { amount: job.materials.MAPA.cal_maxdlr })
|
||||||
t("jobs.labels.threshhold", {
|
|
||||||
amount: job.materials.MAPA.cal_maxdlr
|
|
||||||
})
|
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
</ResponsiveTable.Summary.Cell>
|
</ResponsiveTable.Summary.Cell>
|
||||||
@@ -190,18 +183,11 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
{t("jobs.labels.mash")}
|
{t("jobs.labels.mash")}
|
||||||
{InstanceRenderManager({
|
{InstanceRenderManager({
|
||||||
imex:
|
imex:
|
||||||
job.materials?.mash &&
|
(job.materials?.mash ?? job.materials?.MASH)?.cal_maxdlr > 0 &&
|
||||||
job.materials.mash.cal_maxdlr &&
|
t("jobs.labels.threshhold", { amount: (job.materials.mash ?? job.materials.MASH).cal_maxdlr }),
|
||||||
job.materials.mash.cal_maxdlr > 0 &&
|
|
||||||
t("jobs.labels.threshhold", {
|
|
||||||
amount: job.materials.mash.cal_maxdlr
|
|
||||||
}),
|
|
||||||
rome:
|
rome:
|
||||||
job.materials?.MASH &&
|
job.materials?.MASH?.cal_maxdlr !== undefined &&
|
||||||
job.materials.MASH.cal_maxdlr !== undefined &&
|
t("jobs.labels.threshhold", { amount: job.materials.MASH.cal_maxdlr })
|
||||||
t("jobs.labels.threshhold", {
|
|
||||||
amount: job.materials.MASH.cal_maxdlr
|
|
||||||
})
|
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
</ResponsiveTable.Summary.Cell>
|
</ResponsiveTable.Summary.Cell>
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export function JobsAdminClass({ bodyshop, job }) {
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Popconfirm title={t("jobs.labels.changeclass")} onConfirm={() => form.submit()}>
|
<Popconfirm title={t("jobs.labels.changeclass")} onConfirm={() => form.submit()}>
|
||||||
<Button loading={loading}>{t("general.actions.save")}</Button>
|
<Button loading={loading} type="primary">
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button loading={loading} type="primary" onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<div>{t("jobs.labels.associationwarning")}</div>
|
<div>{t("jobs.labels.associationwarning")}</div>
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button loading={loading} type="primary" onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
<div>{t("jobs.labels.associationwarning")}</div>
|
<div>{t("jobs.labels.associationwarning")}</div>
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button loading={loading} type="primary" onClick={() => form.submit()}>
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
showSearch={{
|
showSearch={{
|
||||||
optionFilterProp: "children",
|
optionFilterProp: "children",
|
||||||
filterOption: (input, option) =>
|
filterOption: (input, option) =>
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0
|
||||||
}}
|
}}
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
||||||
@@ -166,7 +166,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
showSearch={{
|
showSearch={{
|
||||||
optionFilterProp: "children",
|
optionFilterProp: "children",
|
||||||
filterOption: (input, option) =>
|
filterOption: (input, option) =>
|
||||||
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0
|
||||||
}}
|
}}
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
options={bodyshop.md_responsibility_centers.profits.map((p) => ({
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
technician: selectTechnician
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getRequestErrorMessage = (error) => error?.response?.data?.error || error?.message || "";
|
||||||
|
|
||||||
export function PayrollLaborAllocationsTable({
|
export function PayrollLaborAllocationsTable({
|
||||||
jobId,
|
jobId,
|
||||||
joblines,
|
joblines,
|
||||||
@@ -43,16 +45,23 @@ export function PayrollLaborAllocationsTable({
|
|||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
useEffect(() => {
|
const loadTotals = async () => {
|
||||||
async function CalculateTotals() {
|
try {
|
||||||
const { data } = await axios.post("/payroll/calculatelabor", {
|
const { data } = await axios.post("/payroll/calculatelabor", {
|
||||||
jobid: jobId
|
jobid: jobId
|
||||||
});
|
});
|
||||||
setTotals(data);
|
setTotals(data);
|
||||||
|
} catch (error) {
|
||||||
|
setTotals([]);
|
||||||
|
notification.error({
|
||||||
|
title: getRequestErrorMessage(error)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!!joblines && !!timetickets && !!bodyshop) {
|
if (!!joblines && !!timetickets && !!bodyshop) {
|
||||||
CalculateTotals();
|
loadTotals();
|
||||||
}
|
}
|
||||||
if (!jobId) setTotals([]);
|
if (!jobId) setTotals([]);
|
||||||
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
||||||
@@ -210,28 +219,36 @@ export function PayrollLaborAllocationsTable({
|
|||||||
<Button
|
<Button
|
||||||
disabled={!hasTimeTicketAccess}
|
disabled={!hasTimeTicketAccess}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const response = await axios.post("/payroll/payall", {
|
try {
|
||||||
jobid: jobId
|
const response = await axios.post("/payroll/payall", {
|
||||||
});
|
jobid: jobId
|
||||||
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
if (response.data.success !== false) {
|
if (response.data.success !== false) {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("timetickets.successes.payall")
|
title: t("timetickets.successes.payall")
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
title: t("timetickets.errors.payall", {
|
||||||
|
error: response.data.error
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refetch) refetch();
|
||||||
} else {
|
} else {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("timetickets.errors.payall", {
|
title: t("timetickets.errors.payall", {
|
||||||
error: response.data.error
|
error: JSON.stringify("")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
if (refetch) refetch();
|
|
||||||
} else {
|
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("timetickets.errors.payall", {
|
title: t("timetickets.errors.payall", {
|
||||||
error: JSON.stringify("")
|
error: getRequestErrorMessage(error)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -241,10 +258,7 @@ export function PayrollLaborAllocationsTable({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const { data } = await axios.post("/payroll/calculatelabor", {
|
await loadTotals();
|
||||||
jobid: jobId
|
|
||||||
});
|
|
||||||
setTotals(data);
|
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
icon={<SyncOutlined />}
|
icon={<SyncOutlined />}
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ export function PartsOrderListTableComponent({
|
|||||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||||
|
|
||||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||||
|
|
||||||
|
const enrichedPartsOrders = parts_orders.map((order) => ({
|
||||||
|
...order,
|
||||||
|
invoice_number: order.bill?.invoice_number
|
||||||
|
}));
|
||||||
|
|
||||||
const { refetch } = billsQuery;
|
const { refetch } = billsQuery;
|
||||||
|
|
||||||
const recordActions = (record, showView = false) => (
|
const recordActions = (record, showView = false) => (
|
||||||
@@ -222,7 +228,12 @@ export function PartsOrderListTableComponent({
|
|||||||
dataIndex: "order_number",
|
dataIndex: "order_number",
|
||||||
key: "order_number",
|
key: "order_number",
|
||||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||||
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
|
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>
|
||||||
|
{record.order_number} {record.invoice_number && `(${record.invoice_number})`}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("parts_orders.fields.order_date"),
|
title: t("parts_orders.fields.order_date"),
|
||||||
@@ -272,10 +283,10 @@ export function PartsOrderListTableComponent({
|
|||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredPartsOrders = parts_orders
|
const filteredPartsOrders = enrichedPartsOrders
|
||||||
? searchText === ""
|
? searchText === ""
|
||||||
? parts_orders
|
? enrichedPartsOrders
|
||||||
: parts_orders.filter(
|
: enrichedPartsOrders.filter(
|
||||||
(b) =>
|
(b) =>
|
||||||
(b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
|
(b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase())
|
(b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
import { MailOutlined, PrinterOutlined, SignatureFilled } from "@ant-design/icons";
|
||||||
import { Space, Spin } from "antd";
|
import { Space, Spin } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -10,6 +10,8 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
|
|||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import axios from "axios";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
printCenterModal: selectPrintCenter,
|
printCenterModal: selectPrintCenter,
|
||||||
@@ -17,9 +19,25 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
technician: selectTechnician
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = () => ({});
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setEsignatureContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "esignature"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, disabled, technician }) {
|
export function PrintCenterItemComponent({
|
||||||
|
printCenterModal,
|
||||||
|
setEsignatureContext,
|
||||||
|
item,
|
||||||
|
id,
|
||||||
|
bodyshop,
|
||||||
|
disabled,
|
||||||
|
technician
|
||||||
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { context } = printCenterModal;
|
const { context } = printCenterModal;
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
@@ -39,6 +57,30 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const esignatureGenerate = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { token, documentId, evnelopeId }
|
||||||
|
} = await axios.post("/esign/new", {
|
||||||
|
name: item.key,
|
||||||
|
jobid: id,
|
||||||
|
context,
|
||||||
|
bodyshop,
|
||||||
|
templateObject: {
|
||||||
|
name: item.key,
|
||||||
|
variables: { id: id }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setEsignatureContext({ context: { token, documentId, evnelopeId, jobid: id } });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
disabled ||
|
disabled ||
|
||||||
(item.featureNameRestricted && !HasFeatureAccess({ featureName: item.featureNameRestricted, bodyshop }))
|
(item.featureNameRestricted && !HasFeatureAccess({ featureName: item.featureNameRestricted, bodyshop }))
|
||||||
@@ -54,6 +96,7 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
|
|||||||
<li>
|
<li>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{item.title}
|
{item.title}
|
||||||
|
<SignatureFilled onClick={esignatureGenerate} />
|
||||||
<PrinterOutlined onClick={renderToNewWindow} />
|
<PrinterOutlined onClick={renderToNewWindow} />
|
||||||
{!technician ? (
|
{!technician ? (
|
||||||
<MailOutlined
|
<MailOutlined
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client/react";
|
import { useMutation } from "@apollo/client/react";
|
||||||
import { Button, Input, Popover, Tooltip } from "antd";
|
import { Button, Input, Popover, Tooltip } from "antd";
|
||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaRegStickyNote } from "react-icons/fa";
|
import { FaRegStickyNote } from "react-icons/fa";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
@@ -9,10 +9,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
|
|
||||||
export default function ProductionListColumnComment({ record, usePortal = false }) {
|
export default function ProductionListColumnComment({ record, usePortal = false }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [note, setNote] = useState(record.comment || "");
|
const [note, setNote] = useState(record.comment || "");
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const textAreaRef = useRef(null);
|
||||||
|
const rafIdRef = useRef(null);
|
||||||
|
|
||||||
const [updateAlert] = useMutation(UPDATE_JOB);
|
const [updateAlert] = useMutation(UPDATE_JOB);
|
||||||
|
|
||||||
@@ -38,23 +38,35 @@ export default function ProductionListColumnComment({ record, usePortal = false
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenChange = (flag) => {
|
const handleOpenChange = (flag) => {
|
||||||
|
if (rafIdRef.current) {
|
||||||
|
cancelAnimationFrame(rafIdRef.current);
|
||||||
|
rafIdRef.current = null;
|
||||||
|
}
|
||||||
setOpen(flag);
|
setOpen(flag);
|
||||||
if (flag) setNote(record.comment || "");
|
if (flag) {
|
||||||
|
setNote(record.comment || "");
|
||||||
|
rafIdRef.current = requestAnimationFrame(() => {
|
||||||
|
rafIdRef.current = null;
|
||||||
|
if (textAreaRef.current?.focus) {
|
||||||
|
try {
|
||||||
|
textAreaRef.current.focus({ preventScroll: true });
|
||||||
|
} catch {
|
||||||
|
textAreaRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div style={{ width: "30em" }} onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
style={{ width: "30em" }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
id={`job-comment-${record.id}`}
|
id={`job-comment-${record.id}`}
|
||||||
name="comment"
|
name="comment"
|
||||||
rows={5}
|
rows={5}
|
||||||
value={note}
|
value={note}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus
|
ref={textAreaRef}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ marginBottom: "1em" }}
|
style={{ marginBottom: "1em" }}
|
||||||
/>
|
/>
|
||||||
@@ -67,13 +79,13 @@ export default function ProductionListColumnComment({ record, usePortal = false
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
open={open}
|
open={open}
|
||||||
content={content}
|
content={content}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{ body: { padding: '12px' } }}
|
styles={{ body: { padding: "12px" } }}
|
||||||
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client/react";
|
import { useMutation } from "@apollo/client/react";
|
||||||
import { Button, Input, Popover, Space } from "antd";
|
import { Button, Input, Popover, Space } from "antd";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaRegStickyNote } from "react-icons/fa";
|
import { FaRegStickyNote } from "react-icons/fa";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
@@ -20,6 +20,8 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [note, setNote] = useState(record.production_vars?.note || "");
|
const [note, setNote] = useState(record.production_vars?.note || "");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const textAreaRef = useRef(null);
|
||||||
|
const rafIdRef = useRef(null);
|
||||||
|
|
||||||
const [updateAlert] = useMutation(UPDATE_JOB);
|
const [updateAlert] = useMutation(UPDATE_JOB);
|
||||||
|
|
||||||
@@ -52,25 +54,37 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP
|
|||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(flag) => {
|
(flag) => {
|
||||||
|
if (rafIdRef.current) {
|
||||||
|
cancelAnimationFrame(rafIdRef.current);
|
||||||
|
rafIdRef.current = null;
|
||||||
|
}
|
||||||
setOpen(flag);
|
setOpen(flag);
|
||||||
if (flag) setNote(record.production_vars?.note || "");
|
if (flag) {
|
||||||
|
setNote(record.production_vars?.note || "");
|
||||||
|
rafIdRef.current = requestAnimationFrame(() => {
|
||||||
|
rafIdRef.current = null;
|
||||||
|
if (textAreaRef.current?.focus) {
|
||||||
|
try {
|
||||||
|
textAreaRef.current.focus({ preventScroll: true });
|
||||||
|
} catch {
|
||||||
|
textAreaRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[record]
|
[record]
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div style={{ width: "30em" }} onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
|
||||||
style={{ width: "30em" }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
id={`job-production-note-${record.id}`}
|
id={`job-production-note-${record.id}`}
|
||||||
name="production_note"
|
name="production_note"
|
||||||
rows={5}
|
rows={5}
|
||||||
value={note}
|
value={note}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
autoFocus
|
ref={textAreaRef}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ marginBottom: "1em" }}
|
style={{ marginBottom: "1em" }}
|
||||||
/>
|
/>
|
||||||
@@ -96,13 +110,13 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
open={open}
|
open={open}
|
||||||
content={content}
|
content={content}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{ body: { padding: '12px' } }}
|
styles={{ body: { padding: "12px" } }}
|
||||||
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
{...(usePortal ? { getPopupContainer: (trigger) => trigger.parentElement || document.body } : {})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const ret = {
|
|||||||
"jobs:partsqueue": 4,
|
"jobs:partsqueue": 4,
|
||||||
"jobs:checklist-view": 2,
|
"jobs:checklist-view": 2,
|
||||||
"jobs:list-ready": 1,
|
"jobs:list-ready": 1,
|
||||||
|
"jobs:manual-line": 1,
|
||||||
"jobs:void": 5,
|
"jobs:void": 5,
|
||||||
|
|
||||||
"bills:enter": 2,
|
"bills:enter": 2,
|
||||||
|
|||||||
@@ -435,6 +435,19 @@ export function ShopInfoRbacComponent({ bodyshop }) {
|
|||||||
>
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>,
|
</Form.Item>,
|
||||||
|
<Form.Item
|
||||||
|
key="jobs:manual-line"
|
||||||
|
label={t("bodyshop.fields.rbac.jobs.manual-line")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["md_rbac", "jobs:manual-line"]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>,
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="jobs:partsqueue"
|
key="jobs:partsqueue"
|
||||||
label={t("bodyshop.fields.rbac.jobs.partsqueue")}
|
label={t("bodyshop.fields.rbac.jobs.partsqueue")}
|
||||||
|
|||||||
@@ -16,6 +16,43 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoTaskPresets);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoTaskPresets);
|
||||||
|
|
||||||
|
const normalizePercent = (value) => Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000;
|
||||||
|
|
||||||
|
const getTaskPresetAllocationErrors = (presets = [], t) => {
|
||||||
|
const totalsByLaborType = {};
|
||||||
|
|
||||||
|
presets.forEach((preset) => {
|
||||||
|
const percent = normalizePercent(preset?.percent);
|
||||||
|
|
||||||
|
if (!percent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const laborTypes = Array.isArray(preset?.hourstype) ? preset.hourstype : [];
|
||||||
|
|
||||||
|
laborTypes.forEach((laborType) => {
|
||||||
|
if (!laborType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalsByLaborType[laborType] = normalizePercent((totalsByLaborType[laborType] || 0) + percent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.entries(totalsByLaborType)
|
||||||
|
.filter(([, total]) => total > 100)
|
||||||
|
.map(([laborType, total]) => {
|
||||||
|
const translatedLaborType = t(`joblines.fields.lbr_types.${laborType}`);
|
||||||
|
const laborTypeLabel =
|
||||||
|
translatedLaborType === `joblines.fields.lbr_types.${laborType}` ? laborType : translatedLaborType;
|
||||||
|
|
||||||
|
return t("bodyshop.errors.task_preset_allocation_exceeded", {
|
||||||
|
laborType: laborTypeLabel,
|
||||||
|
total
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function ShopInfoTaskPresets({ bodyshop }) {
|
export function ShopInfoTaskPresets({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -39,8 +76,21 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
|
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
|
||||||
<Form.List name={["md_tasks_presets", "presets"]}>
|
<Form.List
|
||||||
{(fields, { add, remove, move }) => {
|
name={["md_tasks_presets", "presets"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator: async (_, presets) => {
|
||||||
|
const allocationErrors = getTaskPresetAllocationErrors(presets, t);
|
||||||
|
|
||||||
|
if (allocationErrors.length > 0) {
|
||||||
|
throw new Error(allocationErrors.join(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(fields, { add, remove, move }, { errors }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -189,6 +239,7 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Form, Input, InputNumber, Space, Switch } from "antd";
|
import { Button, Card, Form, Input, InputNumber, Select, Space, Switch, Typography } from "antd";
|
||||||
|
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -26,10 +26,32 @@ import { useNotification } from "../../contexts/Notifications/notificationContex
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = () => ({
|
const mapDispatchToProps = () => ({});
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
|
const LABOR_TYPES = ["LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"];
|
||||||
|
|
||||||
|
const PAYOUT_METHOD_OPTIONS = [
|
||||||
|
{ labelKey: "employee_teams.options.hourly", value: "hourly" },
|
||||||
|
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeTeamMember = (teamMember = {}) => ({
|
||||||
|
...teamMember,
|
||||||
|
payout_method: teamMember.payout_method || "hourly",
|
||||||
|
labor_rates: teamMember.labor_rates || {},
|
||||||
|
commission_rates: teamMember.commission_rates || {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const normalizeEmployeeTeam = (employeeTeam = {}) => ({
|
||||||
|
...employeeTeam,
|
||||||
|
employee_team_members: (employeeTeam.employee_team_members || []).map(normalizeTeamMember)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSplitTotal = (teamMembers = []) =>
|
||||||
|
teamMembers.reduce((sum, member) => sum + Number(member?.percentage || 0), 0);
|
||||||
|
|
||||||
|
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||||
|
|
||||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -45,38 +67,73 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.employee_teams_by_pk) form.setFieldsValue(data.employee_teams_by_pk);
|
if (data?.employee_teams_by_pk) {
|
||||||
else {
|
form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk));
|
||||||
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [form, data, search.employeeTeamId]);
|
}, [form, data, search.employeeTeamId]);
|
||||||
|
|
||||||
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
||||||
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
||||||
|
const payoutMethodOptions = PAYOUT_METHOD_OPTIONS.map(({ labelKey, value }) => ({
|
||||||
|
label: t(labelKey),
|
||||||
|
value
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||||
|
const normalizedTeamMembers = employee_team_members.map((teamMember) => {
|
||||||
|
const nextTeamMember = normalizeTeamMember({ ...teamMember });
|
||||||
|
delete nextTeamMember.__typename;
|
||||||
|
return nextTeamMember;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (normalizedTeamMembers.length === 0) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employee_teams.errors.minimum_one_member")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const employeeIds = normalizedTeamMembers.map((teamMember) => teamMember.employeeid).filter(Boolean);
|
||||||
|
const duplicateEmployeeIds = employeeIds.filter((employeeId, index) => employeeIds.indexOf(employeeId) !== index);
|
||||||
|
|
||||||
|
if (duplicateEmployeeIds.length > 0) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employee_teams.errors.duplicate_member")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExactSplitTotal(normalizedTeamMembers)) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employee_teams.errors.allocation_total_exact")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleFinish = async ({ employee_team_members, ...values }) => {
|
|
||||||
if (search.employeeTeamId && search.employeeTeamId !== "new") {
|
if (search.employeeTeamId && search.employeeTeamId !== "new") {
|
||||||
//Update a record.
|
|
||||||
logImEXEvent("shop_employee_update");
|
logImEXEvent("shop_employee_update");
|
||||||
|
|
||||||
const result = await updateEmployeeTeam({
|
const result = await updateEmployeeTeam({
|
||||||
variables: {
|
variables: {
|
||||||
employeeTeamId: search.employeeTeamId,
|
employeeTeamId: search.employeeTeamId,
|
||||||
employeeTeam: values,
|
employeeTeam: values,
|
||||||
teamMemberUpdates: employee_team_members
|
teamMemberUpdates: normalizedTeamMembers
|
||||||
.filter((e) => e.id)
|
.filter((teamMember) => teamMember.id)
|
||||||
.map((e) => {
|
.map((teamMember) => ({
|
||||||
delete e.__typename;
|
where: { id: { _eq: teamMember.id } },
|
||||||
return { where: { id: { _eq: e.id } }, _set: e };
|
_set: teamMember
|
||||||
}),
|
})),
|
||||||
teamMemberInserts: employee_team_members
|
teamMemberInserts: normalizedTeamMembers
|
||||||
.filter((e) => e.id === null || e.id === undefined)
|
.filter((teamMember) => teamMember.id === null || teamMember.id === undefined)
|
||||||
.map((e) => ({ ...e, teamid: search.employeeTeamId })),
|
.map((teamMember) => ({ ...teamMember, teamid: search.employeeTeamId })),
|
||||||
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members.filter(
|
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members
|
||||||
(e) => !employee_team_members.find((etm) => etm.id === e.id)
|
.filter((teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id))
|
||||||
)
|
.map((teamMember) => teamMember.id)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
@@ -89,20 +146,19 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//New record, insert it.
|
|
||||||
logImEXEvent("shop_employee_insert");
|
logImEXEvent("shop_employee_insert");
|
||||||
|
|
||||||
insertEmployeeTeam({
|
insertEmployeeTeam({
|
||||||
variables: {
|
variables: {
|
||||||
employeeTeam: {
|
employeeTeam: {
|
||||||
...values,
|
...values,
|
||||||
employee_team_members: { data: employee_team_members },
|
employee_team_members: { data: normalizedTeamMembers },
|
||||||
bodyshopid: bodyshop.id
|
bodyshopid: bodyshop.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_TEAMS"]
|
refetchQueries: ["QUERY_TEAMS"]
|
||||||
}).then((r) => {
|
}).then((response) => {
|
||||||
search.employeeTeamId = r.data.insert_employee_teams_one.id;
|
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
||||||
history({ search: querystring.stringify(search) });
|
history({ search: querystring.stringify(search) });
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
@@ -130,7 +186,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -145,7 +200,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -169,207 +223,61 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("employee_teams.fields.percentage")}
|
label={t("employee_teams.fields.allocation_percentage")}
|
||||||
key={`${index}`}
|
key={`${index}`}
|
||||||
name={[field.name, "percentage"]}
|
name={[field.name, "percentage"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={2} />
|
<InputNumber min={0} max={100} precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.lbr_types.LAA")}
|
label={t("employee_teams.fields.payout_method")}
|
||||||
key={`${index}`}
|
key={`${index}-payout-method`}
|
||||||
name={[field.name, "labor_rates", "LAA"]}
|
name={[field.name, "payout_method"]}
|
||||||
|
initialValue="hourly"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput />
|
<Select options={payoutMethodOptions} />
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAB")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAB"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAD")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAD"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAE")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAE"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item noStyle dependencies={[["employee_team_members", field.name, "payout_method"]]}>
|
||||||
|
{() => {
|
||||||
|
const payoutMethod =
|
||||||
|
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly";
|
||||||
|
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||||
|
|
||||||
<Form.Item
|
return LABOR_TYPES.map((laborType) => (
|
||||||
label={t("joblines.fields.lbr_types.LAF")}
|
<Form.Item
|
||||||
key={`${index}`}
|
label={payoutMethod === "commission" ? `${t(`joblines.fields.lbr_types.${laborType}`)} %` : t(`joblines.fields.lbr_types.${laborType}`)}
|
||||||
name={[field.name, "labor_rates", "LAF"]}
|
key={`${index}-${fieldName}-${laborType}`}
|
||||||
rules={[
|
name={[field.name, fieldName, laborType]}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
required: true
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<CurrencyInput />
|
{payoutMethod === "commission" ? (
|
||||||
</Form.Item>
|
<InputNumber min={0} max={100} precision={2} />
|
||||||
<Form.Item
|
) : (
|
||||||
label={t("joblines.fields.lbr_types.LAG")}
|
<CurrencyInput />
|
||||||
key={`${index}`}
|
)}
|
||||||
name={[field.name, "labor_rates", "LAG"]}
|
</Form.Item>
|
||||||
rules={[
|
));
|
||||||
{
|
}}
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAM")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAM"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAR")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAR"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAS")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAS"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LAU")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LAU"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LA1")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LA1"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LA2")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LA2"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LA3")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LA3"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("joblines.fields.lbr_types.LA4")}
|
|
||||||
key={`${index}`}
|
|
||||||
name={[field.name, "labor_rates", "LA4"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<DeleteFilled
|
<DeleteFilled
|
||||||
@@ -386,13 +294,32 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add();
|
add({
|
||||||
|
percentage: 0,
|
||||||
|
payout_method: "hourly",
|
||||||
|
labor_rates: {},
|
||||||
|
commission_rates: {}
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
{t("employee_teams.actions.newmember")}
|
{t("employee_teams.actions.newmember")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item noStyle shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
const teamMembers = form.getFieldValue(["employee_team_members"]) || [];
|
||||||
|
const splitTotal = getSplitTotal(teamMembers);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Text type={hasExactSplitTotal(teamMembers) ? undefined : "danger"}>
|
||||||
|
{t("employee_teams.labels.allocation_total", {
|
||||||
|
total: splitTotal.toFixed(2)
|
||||||
|
})}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -66,10 +66,9 @@ export function TechClockInContainer({ setTimeTicketContext, technician, bodysho
|
|||||||
employeeid: technician.id,
|
employeeid: technician.id,
|
||||||
date:
|
date:
|
||||||
typeof bodyshop.timezone === "string"
|
typeof bodyshop.timezone === "string"
|
||||||
? // TODO: Client Update - This may be broken
|
? dayjs(theTime).tz(bodyshop.timezone).format("YYYY-MM-DD")
|
||||||
dayjs.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD")
|
|
||||||
: typeof bodyshop.timezone === "number"
|
: typeof bodyshop.timezone === "number"
|
||||||
? dayjs(theTime).format("YYYY-MM-DD").utcOffset(bodyshop.timezone)
|
? dayjs(theTime).utcOffset(bodyshop.timezone).format("YYYY-MM-DD")
|
||||||
: dayjs(theTime).format("YYYY-MM-DD"),
|
: dayjs(theTime).format("YYYY-MM-DD"),
|
||||||
clockon: dayjs(theTime),
|
clockon: dayjs(theTime),
|
||||||
jobid: values.jobid,
|
jobid: values.jobid,
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
||||||
const breakpoints = Grid.useBreakpoint();
|
const screens = Grid.useBreakpoint();
|
||||||
const selectedBreakpoint = Object.entries(breakpoints)
|
|
||||||
.filter(([, isOn]) => !!isOn)
|
|
||||||
.slice(-1)[0];
|
|
||||||
|
|
||||||
const bpoints = {
|
const bpoints = {
|
||||||
xs: "100%",
|
xs: "100%",
|
||||||
@@ -36,10 +33,16 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
md: "100%",
|
md: "100%",
|
||||||
lg: "100%",
|
lg: "100%",
|
||||||
xl: "90%",
|
xl: "90%",
|
||||||
xxl: "85%"
|
xxl: "90%"
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
let drawerPercentage = "100%";
|
||||||
|
if (screens.xxl) drawerPercentage = bpoints.xxl;
|
||||||
|
else if (screens.xl) drawerPercentage = bpoints.xl;
|
||||||
|
else if (screens.lg) drawerPercentage = bpoints.lg;
|
||||||
|
else if (screens.md) drawerPercentage = bpoints.md;
|
||||||
|
else if (screens.sm) drawerPercentage = bpoints.sm;
|
||||||
|
else if (screens.xs) drawerPercentage = bpoints.xs;
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|||||||
@@ -35,7 +35,15 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
|||||||
<JobSearchSelectComponent convertedOnly={true} notExported={true} />
|
<JobSearchSelectComponent convertedOnly={true} notExported={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Form.Item name="task" label={t("timetickets.labels.task")}>
|
<Form.Item
|
||||||
|
name="task"
|
||||||
|
label={t("timetickets.labels.task")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Spin />
|
<Spin />
|
||||||
) : (
|
) : (
|
||||||
@@ -93,6 +101,8 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
|||||||
<th>{t("timetickets.fields.cost_center")}</th>
|
<th>{t("timetickets.fields.cost_center")}</th>
|
||||||
<th>{t("timetickets.fields.ciecacode")}</th>
|
<th>{t("timetickets.fields.ciecacode")}</th>
|
||||||
<th>{t("timetickets.fields.productivehrs")}</th>
|
<th>{t("timetickets.fields.productivehrs")}</th>
|
||||||
|
<th>{t("timetickets.fields.rate")}</th>
|
||||||
|
<th>{t("timetickets.fields.amount")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -118,6 +128,16 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
|||||||
<ReadOnlyFormItemComponent />
|
<ReadOnlyFormItemComponent />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -90,7 +90,12 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
|||||||
if (actions?.refetch) actions.refetch();
|
if (actions?.refetch) actions.refetch();
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
} else if (handleFinish === false) {
|
} else if (handleFinish === false) {
|
||||||
form.setFieldsValue({ timetickets: data.ticketsToInsert });
|
form.setFieldsValue({
|
||||||
|
timetickets: (data.ticketsToInsert || []).map((ticket) => ({
|
||||||
|
...ticket,
|
||||||
|
payoutamount: Number(ticket.productivehrs || 0) * Number(ticket.rate || 0)
|
||||||
|
}))
|
||||||
|
});
|
||||||
setUnassignedHours(data.unassignedHours);
|
setUnassignedHours(data.unassignedHours);
|
||||||
} else {
|
} else {
|
||||||
notification.error({
|
notification.error({
|
||||||
@@ -101,7 +106,9 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("timetickets.errors.creating", { message: error.message })
|
title: t("timetickets.errors.creating", {
|
||||||
|
message: error.response?.data?.error || error.message
|
||||||
|
})
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -130,7 +130,15 @@ export function TtApprovalsListComponent({
|
|||||||
key: "memo",
|
key: "memo",
|
||||||
sorter: (a, b) => alphaSort(a.memo, b.memo),
|
sorter: (a, b) => alphaSort(a.memo, b.memo),
|
||||||
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
|
||||||
render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo)
|
render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("timetickets.fields.task_name"),
|
||||||
|
dataIndex: "task_name",
|
||||||
|
key: "task_name",
|
||||||
|
sorter: (a, b) => alphaSort(a.task_name, b.task_name),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "task_name" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => record.task_name || ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("timetickets.fields.clockon"),
|
title: t("timetickets.fields.clockon"),
|
||||||
@@ -140,12 +148,12 @@ export function TtApprovalsListComponent({
|
|||||||
render: (text, record) => <DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
render: (text, record) => <DateTimeFormatter>{record.clockon}</DateTimeFormatter>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Pay",
|
title: t("timetickets.fields.pay"),
|
||||||
dataIndex: "pay",
|
dataIndex: "pay",
|
||||||
key: "pay",
|
key: "pay",
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
Dinero({ amount: Math.round(record.rate * 100) })
|
Dinero({ amount: Math.round((record.rate || 0) * 100) })
|
||||||
.multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
|
.multiply(record.flat_rate ? record.productivehrs || 0 : record.actualhrs || 0)
|
||||||
.toFormat("$0.00")
|
.toFormat("$0.00")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -184,7 +192,7 @@ export function TtApprovalsListComponent({
|
|||||||
<ResponsiveTable
|
<ResponsiveTable
|
||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
mobileColumnKeys={["ro_number", "date", "employeeid", "cost_center"]}
|
mobileColumnKeys={["ro_number", "date", "employeeid", "cost_center", "task_name"]}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
scroll={{
|
scroll={{
|
||||||
x: true
|
x: true
|
||||||
|
|||||||
@@ -18,7 +18,15 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
authLevel: selectAuthLevel
|
authLevel: selectAuthLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabled, authLevel }) {
|
export function TtApproveButton({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
selectedTickets,
|
||||||
|
disabled,
|
||||||
|
authLevel,
|
||||||
|
completedCallback,
|
||||||
|
refetch
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
@@ -54,6 +62,12 @@ export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabl
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (typeof completedCallback === "function") {
|
||||||
|
completedCallback([]);
|
||||||
|
}
|
||||||
|
if (typeof refetch === "function") {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("timetickets.successes.created")
|
title: t("timetickets.successes.created")
|
||||||
});
|
});
|
||||||
@@ -68,8 +82,6 @@ export function TtApproveButton({ bodyshop, currentUser, selectedTickets, disabl
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!!completedCallback) completedCallback([]);
|
|
||||||
// if (!!loadingCallback) loadingCallback(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export const QUERY_PARTS_BILLS_BY_JOBID = gql`
|
|||||||
order_number
|
order_number
|
||||||
comments
|
comments
|
||||||
user_email
|
user_email
|
||||||
|
bill {
|
||||||
|
id
|
||||||
|
invoice_number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
parts_dispatch(where: { jobid: { _eq: $jobid } }) {
|
parts_dispatch(where: { jobid: { _eq: $jobid } }) {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
id
|
id
|
||||||
employeeid
|
employeeid
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,6 +287,8 @@ export const UPDATE_SHOP = gql`
|
|||||||
id
|
id
|
||||||
employeeid
|
employeeid
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const QUERY_TEAMS = gql`
|
|||||||
id
|
id
|
||||||
employeeid
|
employeeid
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
id
|
id
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +44,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
id
|
id
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +58,8 @@ export const UPDATE_EMPLOYEE_TEAM = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
id
|
id
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +77,8 @@ export const INSERT_EMPLOYEE_TEAM = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
id
|
id
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +96,8 @@ export const QUERY_EMPLOYEE_TEAM_BY_ID = gql`
|
|||||||
employeeid
|
employeeid
|
||||||
id
|
id
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
percentage
|
percentage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1375,6 +1375,9 @@ export const QUERY_JOB_FOR_DUPE = gql`
|
|||||||
agt_ph2x
|
agt_ph2x
|
||||||
area_of_damage
|
area_of_damage
|
||||||
cat_no
|
cat_no
|
||||||
|
cieca_pfl
|
||||||
|
cieca_pfo
|
||||||
|
cieca_pft
|
||||||
cieca_stl
|
cieca_stl
|
||||||
cieca_ttl
|
cieca_ttl
|
||||||
clm_addr1
|
clm_addr1
|
||||||
@@ -1452,6 +1455,7 @@ export const QUERY_JOB_FOR_DUPE = gql`
|
|||||||
labor_rate_desc
|
labor_rate_desc
|
||||||
labor_rate_id
|
labor_rate_id
|
||||||
local_tax_rate
|
local_tax_rate
|
||||||
|
materials
|
||||||
other_amount_payable
|
other_amount_payable
|
||||||
owner_owing
|
owner_owing
|
||||||
ownerid
|
ownerid
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
|
|||||||
id
|
id
|
||||||
clockon
|
clockon
|
||||||
clockoff
|
clockoff
|
||||||
|
created_by
|
||||||
employeeid
|
employeeid
|
||||||
productivehrs
|
productivehrs
|
||||||
actualhrs
|
actualhrs
|
||||||
@@ -267,6 +268,9 @@ export const INSERT_TIME_TICKET_AND_APPROVE = gql`
|
|||||||
date
|
date
|
||||||
memo
|
memo
|
||||||
flat_rate
|
flat_rate
|
||||||
|
task_name
|
||||||
|
payout_context
|
||||||
|
ttapprovalqueueid
|
||||||
commited_by
|
commited_by
|
||||||
committed_at
|
committed_at
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ export const QUERY_ALL_TT_APPROVALS_PAGINATED = gql`
|
|||||||
ciecacode
|
ciecacode
|
||||||
cost_center
|
cost_center
|
||||||
date
|
date
|
||||||
|
memo
|
||||||
|
flat_rate
|
||||||
|
clockon
|
||||||
|
clockoff
|
||||||
rate
|
rate
|
||||||
|
created_by
|
||||||
|
task_name
|
||||||
|
payout_context
|
||||||
}
|
}
|
||||||
tt_approval_queue_aggregate {
|
tt_approval_queue_aggregate {
|
||||||
aggregate {
|
aggregate {
|
||||||
@@ -42,9 +49,16 @@ export const INSERT_NEW_TT_APPROVALS = gql`
|
|||||||
productivehrs
|
productivehrs
|
||||||
actualhrs
|
actualhrs
|
||||||
ciecacode
|
ciecacode
|
||||||
|
cost_center
|
||||||
date
|
date
|
||||||
memo
|
memo
|
||||||
flat_rate
|
flat_rate
|
||||||
|
rate
|
||||||
|
clockon
|
||||||
|
clockoff
|
||||||
|
created_by
|
||||||
|
task_name
|
||||||
|
payout_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +79,11 @@ export const QUERY_TT_APPROVALS_BY_IDS = gql`
|
|||||||
ciecacode
|
ciecacode
|
||||||
bodyshopid
|
bodyshopid
|
||||||
cost_center
|
cost_center
|
||||||
|
clockon
|
||||||
|
clockoff
|
||||||
|
created_by
|
||||||
|
task_name
|
||||||
|
payout_context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -142,13 +142,13 @@ export function ExportLogsPageComponent() {
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{message.map((m, idx) => (
|
{message.map((m, idx) => (
|
||||||
<li key={idx}>{m}</li>
|
<li key={idx}>{typeof m === "object" ? JSON.stringify(m) : m}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <div>{record.message}</div>;
|
return <div>{typeof message === "object" ? JSON.stringify(message) : message}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
|||||||
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
||||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||||
|
import EsignatureModalContainer from "../../components/esignature-modal/esignature-modal.container.jsx";
|
||||||
|
|
||||||
const PrintCenterModalContainer = lazyDev(
|
const PrintCenterModalContainer = lazyDev(
|
||||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||||
@@ -68,7 +69,9 @@ const FeatureRequestPage = lazyDev(() => import("../feature-request/feature-requ
|
|||||||
const JobCostingModal = lazyDev(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
const JobCostingModal = lazyDev(() => import("../../components/job-costing-modal/job-costing-modal.container"));
|
||||||
const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container"));
|
const ReportCenterModal = lazyDev(() => import("../../components/report-center-modal/report-center-modal.container"));
|
||||||
const BillEnterModalContainer = lazyDev(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
|
const BillEnterModalContainer = lazyDev(() => import("../../components/bill-enter-modal/bill-enter-modal.container"));
|
||||||
const TimeTicketModalContainer = lazyDev(() => import("../../components/time-ticket-modal/time-ticket-modal.container"));
|
const TimeTicketModalContainer = lazyDev(
|
||||||
|
() => import("../../components/time-ticket-modal/time-ticket-modal.container")
|
||||||
|
);
|
||||||
const TimeTicketModalTask = lazyDev(
|
const TimeTicketModalTask = lazyDev(
|
||||||
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||||
);
|
);
|
||||||
@@ -110,7 +113,9 @@ const TtApprovals = lazyDev(() => import("../tt-approvals/tt-approvals.page.cont
|
|||||||
const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.jsx"));
|
const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.jsx"));
|
||||||
const AllTasksPage = lazyDev(() => import("../tasks/allTasksPageContainer.jsx"));
|
const AllTasksPage = lazyDev(() => import("../tasks/allTasksPageContainer.jsx"));
|
||||||
|
|
||||||
const TaskUpsertModalContainer = lazyDev(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
const TaskUpsertModalContainer = lazyDev(
|
||||||
|
() => import("../../components/task-upsert-modal/task-upsert-modal.container")
|
||||||
|
);
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -178,6 +183,7 @@ export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, cu
|
|||||||
<TaskUpsertModalContainer />
|
<TaskUpsertModalContainer />
|
||||||
<BreadCrumbs />
|
<BreadCrumbs />
|
||||||
<BillEnterModalContainer />
|
<BillEnterModalContainer />
|
||||||
|
<EsignatureModalContainer />
|
||||||
<JobCostingModal />
|
<JobCostingModal />
|
||||||
<ReportCenterModal />
|
<ReportCenterModal />
|
||||||
<EmailOverlayContainer />
|
<EmailOverlayContainer />
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ const INITIAL_STATE = {
|
|||||||
contractFinder: { ...baseModal },
|
contractFinder: { ...baseModal },
|
||||||
inventoryUpsert: { ...baseModal },
|
inventoryUpsert: { ...baseModal },
|
||||||
ca_bc_eftTableConvert: { ...baseModal },
|
ca_bc_eftTableConvert: { ...baseModal },
|
||||||
cardPayment: { ...baseModal }
|
cardPayment: { ...baseModal },
|
||||||
|
esignature: { ...baseModal }
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalsReducer = (state = INITIAL_STATE, action) => {
|
const modalsReducer = (state = INITIAL_STATE, action) => {
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ export const selectInventoryUpsert = createSelector([selectModals], (modals) =>
|
|||||||
export const selectCaBcEtfTableConvert = createSelector([selectModals], (modals) => modals.ca_bc_eftTableConvert);
|
export const selectCaBcEtfTableConvert = createSelector([selectModals], (modals) => modals.ca_bc_eftTableConvert);
|
||||||
|
|
||||||
export const selectCardPayment = createSelector([selectModals], (modals) => modals.cardPayment);
|
export const selectCardPayment = createSelector([selectModals], (modals) => modals.cardPayment);
|
||||||
|
export const selectEsignature = createSelector([selectModals], (modals) => modals.esignature);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { combineReducers } from "redux";
|
import { combineReducers } from "redux";
|
||||||
import { persistReducer } from "redux-persist";
|
import { persistReducer } from "redux-persist";
|
||||||
import storage from "redux-persist/lib/storage";
|
import storageModule from "redux-persist/lib/storage";
|
||||||
import { withReduxStateSync } from "redux-state-sync";
|
import { withReduxStateSync } from "redux-state-sync";
|
||||||
import applicationReducer from "./application/application.reducer";
|
import applicationReducer from "./application/application.reducer";
|
||||||
import emailReducer from "./email/email.reducer";
|
import emailReducer from "./email/email.reducer";
|
||||||
@@ -11,6 +11,8 @@ import techReducer from "./tech/tech.reducer";
|
|||||||
import userReducer from "./user/user.reducer";
|
import userReducer from "./user/user.reducer";
|
||||||
import trelloReducer from "./trello/trello.reducer";
|
import trelloReducer from "./trello/trello.reducer";
|
||||||
|
|
||||||
|
const storage = storageModule?.default ?? storageModule;
|
||||||
|
|
||||||
// const persistConfig = {
|
// const persistConfig = {
|
||||||
// key: "root",
|
// key: "root",
|
||||||
// storage,
|
// storage,
|
||||||
|
|||||||
@@ -305,7 +305,8 @@
|
|||||||
"creatingdefaultview": "Error creating default view.",
|
"creatingdefaultview": "Error creating default view.",
|
||||||
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
|
"duplicate_insurance_company": "Duplicate insurance company name. Each insurance company name must be unique",
|
||||||
"loading": "Unable to load shop details. Please call technical support.",
|
"loading": "Unable to load shop details. Please call technical support.",
|
||||||
"saving": "Error encountered while saving. {{message}}"
|
"saving": "Error encountered while saving. {{message}}",
|
||||||
|
"task_preset_allocation_exceeded": "{{laborType}} task preset total is {{total}}% and cannot exceed 100%."
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
"list-active": "Jobs -> List Active",
|
"list-active": "Jobs -> List Active",
|
||||||
"list-all": "Jobs -> List All",
|
"list-all": "Jobs -> List All",
|
||||||
"list-ready": "Jobs -> List Ready",
|
"list-ready": "Jobs -> List Ready",
|
||||||
|
"manual-line": "Jobs -> Manual Line",
|
||||||
"partsqueue": "Jobs -> Parts Queue",
|
"partsqueue": "Jobs -> Parts Queue",
|
||||||
"void": "Jobs -> Void"
|
"void": "Jobs -> Void"
|
||||||
},
|
},
|
||||||
@@ -1174,12 +1176,26 @@
|
|||||||
"new": "New Team",
|
"new": "New Team",
|
||||||
"newmember": "New Team Member"
|
"newmember": "New Team Member"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"allocation_total_exact": "Team allocation must total exactly 100%.",
|
||||||
|
"duplicate_member": "Each employee can only appear once per team.",
|
||||||
|
"minimum_one_member": "Add at least one team member."
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"allocation_percentage": "Allocation %",
|
||||||
"employeeid": "Employee",
|
"employeeid": "Employee",
|
||||||
"max_load": "Max Load",
|
"max_load": "Max Load",
|
||||||
"name": "Team Name",
|
"name": "Team Name",
|
||||||
|
"payout_method": "Payout Method",
|
||||||
"percentage": "Percent"
|
"percentage": "Percent"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"allocation_total": "Allocation Total: {{total}}%"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"commission_percentage": "Commission %",
|
||||||
|
"hourly": "Hourly"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
@@ -2187,6 +2203,7 @@
|
|||||||
"duplicateconfirm": "Are you sure you want to duplicate this Job? Some elements of this Job will not be duplicated.",
|
"duplicateconfirm": "Are you sure you want to duplicate this Job? Some elements of this Job will not be duplicated.",
|
||||||
"emailaudit": "Email Audit Trail",
|
"emailaudit": "Email Audit Trail",
|
||||||
"employeeassignments": "Employee Assignments",
|
"employeeassignments": "Employee Assignments",
|
||||||
|
"esignature": "E-Signature",
|
||||||
"estimatelines": "Estimate Lines",
|
"estimatelines": "Estimate Lines",
|
||||||
"estimator": "Estimator",
|
"estimator": "Estimator",
|
||||||
"existing_jobs": "Existing Jobs",
|
"existing_jobs": "Existing Jobs",
|
||||||
@@ -3373,8 +3390,10 @@
|
|||||||
"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",
|
||||||
"work_in_progress_jobs": "Work in Progress - Jobs",
|
"work_in_progress_jobs": "Work in Progress - Jobs",
|
||||||
"work_in_progress_labour": "Work in Progress - Labor",
|
"work_in_progress_labour": "Work in Progress - Labor (Detail)",
|
||||||
"work_in_progress_payables": "Work in Progress - Payables"
|
"work_in_progress_labour_summary": "Work in Progress - Labor (Summary)",
|
||||||
|
"work_in_progress_payables": "Work in Progress - Payables (Detail)",
|
||||||
|
"work_in_progress_payables_summary": "Work in Progress - Payables (Summary)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
@@ -3605,6 +3624,7 @@
|
|||||||
"employee_team": "Employee Team",
|
"employee_team": "Employee Team",
|
||||||
"flat_rate": "Flat Rate?",
|
"flat_rate": "Flat Rate?",
|
||||||
"memo": "Memo",
|
"memo": "Memo",
|
||||||
|
"pay": "Pay",
|
||||||
"productivehrs": "Productive Hours",
|
"productivehrs": "Productive Hours",
|
||||||
"ro_number": "Job to Post Against",
|
"ro_number": "Job to Post Against",
|
||||||
"task_name": "Task"
|
"task_name": "Task"
|
||||||
|
|||||||
@@ -305,7 +305,8 @@
|
|||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
||||||
"saving": ""
|
"saving": "",
|
||||||
|
"task_preset_allocation_exceeded": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"ReceivableCustomField": "",
|
"ReceivableCustomField": "",
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
"list-active": "",
|
"list-active": "",
|
||||||
"list-all": "",
|
"list-all": "",
|
||||||
"list-ready": "",
|
"list-ready": "",
|
||||||
|
"manual-line": "",
|
||||||
"partsqueue": "",
|
"partsqueue": "",
|
||||||
"void": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
@@ -1174,12 +1176,26 @@
|
|||||||
"new": "",
|
"new": "",
|
||||||
"newmember": ""
|
"newmember": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"allocation_total_exact": "",
|
||||||
|
"duplicate_member": "",
|
||||||
|
"minimum_one_member": ""
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"allocation_percentage": "",
|
||||||
"employeeid": "",
|
"employeeid": "",
|
||||||
"max_load": "",
|
"max_load": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
"payout_method": "",
|
||||||
"percentage": ""
|
"percentage": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"allocation_total": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"commission_percentage": "",
|
||||||
|
"hourly": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
@@ -2187,6 +2203,7 @@
|
|||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"emailaudit": "",
|
"emailaudit": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"esignature": "",
|
||||||
"estimatelines": "",
|
"estimatelines": "",
|
||||||
"estimator": "",
|
"estimator": "",
|
||||||
"existing_jobs": "Empleos existentes",
|
"existing_jobs": "Empleos existentes",
|
||||||
@@ -3374,7 +3391,9 @@
|
|||||||
"work_in_progress_committed_labour": "",
|
"work_in_progress_committed_labour": "",
|
||||||
"work_in_progress_jobs": "",
|
"work_in_progress_jobs": "",
|
||||||
"work_in_progress_labour": "",
|
"work_in_progress_labour": "",
|
||||||
"work_in_progress_payables": ""
|
"work_in_progress_labour_summary": "",
|
||||||
|
"work_in_progress_payables": "",
|
||||||
|
"work_in_progress_payables_summary": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
@@ -3605,6 +3624,7 @@
|
|||||||
"employee_team": "",
|
"employee_team": "",
|
||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
|
"pay": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
"ro_number": "",
|
"ro_number": "",
|
||||||
"task_name": ""
|
"task_name": ""
|
||||||
|
|||||||
@@ -305,7 +305,8 @@
|
|||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
||||||
"saving": ""
|
"saving": "",
|
||||||
|
"task_preset_allocation_exceeded": ""
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"ReceivableCustomField": "",
|
"ReceivableCustomField": "",
|
||||||
@@ -519,6 +520,7 @@
|
|||||||
"list-active": "",
|
"list-active": "",
|
||||||
"list-all": "",
|
"list-all": "",
|
||||||
"list-ready": "",
|
"list-ready": "",
|
||||||
|
"manual-line": "",
|
||||||
"partsqueue": "",
|
"partsqueue": "",
|
||||||
"void": ""
|
"void": ""
|
||||||
},
|
},
|
||||||
@@ -1174,12 +1176,26 @@
|
|||||||
"new": "",
|
"new": "",
|
||||||
"newmember": ""
|
"newmember": ""
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"allocation_total_exact": "",
|
||||||
|
"duplicate_member": "",
|
||||||
|
"minimum_one_member": ""
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"allocation_percentage": "",
|
||||||
"employeeid": "",
|
"employeeid": "",
|
||||||
"max_load": "",
|
"max_load": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
"payout_method": "",
|
||||||
"percentage": ""
|
"percentage": ""
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"allocation_total": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"commission_percentage": "",
|
||||||
|
"hourly": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
@@ -2187,6 +2203,7 @@
|
|||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"emailaudit": "",
|
"emailaudit": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"esignature": "",
|
||||||
"estimatelines": "",
|
"estimatelines": "",
|
||||||
"estimator": "",
|
"estimator": "",
|
||||||
"existing_jobs": "Emplois existants",
|
"existing_jobs": "Emplois existants",
|
||||||
@@ -3374,7 +3391,9 @@
|
|||||||
"work_in_progress_committed_labour": "",
|
"work_in_progress_committed_labour": "",
|
||||||
"work_in_progress_jobs": "",
|
"work_in_progress_jobs": "",
|
||||||
"work_in_progress_labour": "",
|
"work_in_progress_labour": "",
|
||||||
"work_in_progress_payables": ""
|
"work_in_progress_labour_summary": "",
|
||||||
|
"work_in_progress_payables": "",
|
||||||
|
"work_in_progress_payables_summary": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
@@ -3605,6 +3624,7 @@
|
|||||||
"employee_team": "",
|
"employee_team": "",
|
||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"memo": "",
|
"memo": "",
|
||||||
|
"pay": "",
|
||||||
"productivehrs": "",
|
"productivehrs": "",
|
||||||
"ro_number": "",
|
"ro_number": "",
|
||||||
"task_name": ""
|
"task_name": ""
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { RetryLink } from "@apollo/client/link/retry";
|
|||||||
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
|
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
|
||||||
import { getMainDefinition } from "@apollo/client/utilities";
|
import { getMainDefinition } from "@apollo/client/utilities";
|
||||||
|
|
||||||
import apolloLogger from "apollo-link-logger";
|
import apolloLoggerModule from "apollo-link-logger";
|
||||||
import { createClient } from "graphql-ws";
|
import { createClient } from "graphql-ws";
|
||||||
import { map } from "rxjs/operators";
|
import { map } from "rxjs/operators";
|
||||||
|
|
||||||
import { auth } from "../firebase/firebase.utils";
|
import { auth } from "../firebase/firebase.utils";
|
||||||
import errorLink from "../graphql/apollo-error-handling";
|
import errorLink from "../graphql/apollo-error-handling";
|
||||||
|
|
||||||
|
const apolloLogger = apolloLoggerModule?.default ?? apolloLoggerModule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP transport
|
* HTTP transport
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1717,6 +1717,20 @@ export const TemplateList = (type, context) => {
|
|||||||
group: "jobs",
|
group: "jobs",
|
||||||
featureNameRestricted: "timetickets"
|
featureNameRestricted: "timetickets"
|
||||||
},
|
},
|
||||||
|
work_in_progress_labour_summary: {
|
||||||
|
title: i18n.t("reportcenter.templates.work_in_progress_labour_summary"),
|
||||||
|
description: "",
|
||||||
|
subject: i18n.t("reportcenter.templates.work_in_progress_labour_summary"),
|
||||||
|
key: "work_in_progress_labour_summary",
|
||||||
|
//idtype: "vendor",
|
||||||
|
disabled: false,
|
||||||
|
rangeFilter: {
|
||||||
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
|
field: i18n.t("jobs.fields.date_open")
|
||||||
|
},
|
||||||
|
group: "jobs",
|
||||||
|
featureNameRestricted: "timetickets"
|
||||||
|
},
|
||||||
work_in_progress_committed_labour: {
|
work_in_progress_committed_labour: {
|
||||||
title: i18n.t("reportcenter.templates.work_in_progress_committed_labour"),
|
title: i18n.t("reportcenter.templates.work_in_progress_committed_labour"),
|
||||||
description: "",
|
description: "",
|
||||||
@@ -1746,6 +1760,20 @@ export const TemplateList = (type, context) => {
|
|||||||
group: "jobs",
|
group: "jobs",
|
||||||
featureNameRestricted: "bills"
|
featureNameRestricted: "bills"
|
||||||
},
|
},
|
||||||
|
work_in_progress_payables_summary: {
|
||||||
|
title: i18n.t("reportcenter.templates.work_in_progress_payables_summary"),
|
||||||
|
description: "",
|
||||||
|
subject: i18n.t("reportcenter.templates.work_in_progress_payables_summary"),
|
||||||
|
key: "work_in_progress_payables_summary",
|
||||||
|
//idtype: "vendor",
|
||||||
|
disabled: false,
|
||||||
|
rangeFilter: {
|
||||||
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
|
field: i18n.t("jobs.fields.date_open")
|
||||||
|
},
|
||||||
|
group: "jobs",
|
||||||
|
featureNameRestricted: "bills"
|
||||||
|
},
|
||||||
lag_time: {
|
lag_time: {
|
||||||
title: i18n.t("reportcenter.templates.lag_time"),
|
title: i18n.t("reportcenter.templates.lag_time"),
|
||||||
description: "",
|
description: "",
|
||||||
|
|||||||
@@ -47,6 +47,33 @@ const httpsCerts = {
|
|||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
const pathSeparatorPattern = String.raw`[\\/]`;
|
||||||
|
|
||||||
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const packageChunkTest = (packageNames) => {
|
||||||
|
const names = Array.isArray(packageNames) ? packageNames : [packageNames];
|
||||||
|
|
||||||
|
return new RegExp(
|
||||||
|
`${pathSeparatorPattern}node_modules${pathSeparatorPattern}(?:${names
|
||||||
|
.map((name) => name.split("/").map(escapeRegex).join(pathSeparatorPattern))
|
||||||
|
.join("|")})(?:${pathSeparatorPattern}|$)`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const vendorCodeSplittingGroups = [
|
||||||
|
{ name: "antd", test: packageChunkTest("antd"), priority: 100 },
|
||||||
|
{ name: "react-redux", test: packageChunkTest("react-redux"), priority: 95 },
|
||||||
|
{ name: "redux", test: packageChunkTest("redux"), priority: 90 },
|
||||||
|
{ name: "lodash", test: packageChunkTest("lodash"), priority: 85 },
|
||||||
|
{ name: "@sentry/react", test: packageChunkTest("@sentry/react"), priority: 80 },
|
||||||
|
{ name: "@splitsoftware/splitio-react", test: packageChunkTest("@splitsoftware/splitio-react"), priority: 75 },
|
||||||
|
{ name: "logrocket", test: packageChunkTest("logrocket"), priority: 70 },
|
||||||
|
{ name: "firebase", test: packageChunkTest("@firebase"), priority: 65 },
|
||||||
|
{ name: "markerjs2", test: packageChunkTest("markerjs2"), priority: 60 },
|
||||||
|
{ name: "@apollo/client", test: packageChunkTest("@apollo/client"), priority: 55 },
|
||||||
|
{ name: "libphonenumber-js", test: packageChunkTest("libphonenumber-js"), priority: 50 },
|
||||||
|
{ name: "recharts", test: packageChunkTest("recharts"), priority: 45 }
|
||||||
|
];
|
||||||
|
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
// React Compiler is always enabled for production/test builds
|
// React Compiler is always enabled for production/test builds
|
||||||
@@ -228,27 +255,13 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
|
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rolldownOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
codeSplitting: {
|
||||||
antd: ["antd"],
|
groups: vendorCodeSplittingGroups
|
||||||
"react-redux": ["react-redux"],
|
},
|
||||||
redux: ["redux"],
|
comments: {
|
||||||
lodash: ["lodash"],
|
legal: false
|
||||||
"@sentry/react": ["@sentry/react"],
|
|
||||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
|
||||||
logrocket: ["logrocket"],
|
|
||||||
firebase: [
|
|
||||||
"@firebase/analytics",
|
|
||||||
"@firebase/app",
|
|
||||||
"@firebase/firestore",
|
|
||||||
"@firebase/auth",
|
|
||||||
"@firebase/messaging"
|
|
||||||
],
|
|
||||||
markerjs2: ["markerjs2"],
|
|
||||||
"@apollo/client": ["@apollo/client"],
|
|
||||||
"libphonenumber-js": ["libphonenumber-js"],
|
|
||||||
recharts: ["recharts"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -256,12 +269,6 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
cssMinify: "lightningcss"
|
cssMinify: "lightningcss"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Strip console/debugger in prod to shrink bundles
|
|
||||||
esbuild: {
|
|
||||||
// drop: mode === "production" ? ["console", "debugger"] : [],
|
|
||||||
legalComments: "none" // Remove license comments in production
|
|
||||||
},
|
|
||||||
|
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
"react",
|
"react",
|
||||||
@@ -284,8 +291,8 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
"@firebase/util",
|
"@firebase/util",
|
||||||
"styled-components"
|
"styled-components"
|
||||||
],
|
],
|
||||||
esbuildOptions: {
|
rolldownOptions: {
|
||||||
loader: { ".jsx": "jsx", ".tsx": "tsx" }
|
moduleTypes: { ".jsx": "jsx", ".tsx": "tsx" }
|
||||||
},
|
},
|
||||||
// Force styled-components to be pre-bundled and deduplicated
|
// Force styled-components to be pre-bundled and deduplicated
|
||||||
force: mode === "development"
|
force: mode === "development"
|
||||||
|
|||||||
@@ -2156,10 +2156,12 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -2167,10 +2169,12 @@
|
|||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -2188,10 +2192,12 @@
|
|||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
columns:
|
columns:
|
||||||
|
- commission_rates
|
||||||
- created_at
|
- created_at
|
||||||
- employeeid
|
- employeeid
|
||||||
- id
|
- id
|
||||||
- labor_rates
|
- labor_rates
|
||||||
|
- payout_method
|
||||||
- percentage
|
- percentage
|
||||||
- teamid
|
- teamid
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -6506,6 +6512,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6531,6 +6538,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6565,6 +6573,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- task_name
|
- task_name
|
||||||
@@ -6748,6 +6757,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -6768,6 +6778,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
@@ -6798,6 +6809,7 @@
|
|||||||
- id
|
- id
|
||||||
- jobid
|
- jobid
|
||||||
- memo
|
- memo
|
||||||
|
- payout_context
|
||||||
- productivehrs
|
- productivehrs
|
||||||
- rate
|
- rate
|
||||||
- updated_at
|
- updated_at
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."employee_team_members" add column "payout_method" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."employee_team_members" add column "payout_method" text
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."employee_team_members" add column "commission_rates" jsonb
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."employee_team_members" add column "commission_rates" jsonb
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."timetickets" add column "payout_context" jsonb
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."timetickets" add column "payout_context" jsonb
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."tt_approval_queue" add column "payout_context" jsonb
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."tt_approval_queue" add column "payout_context" jsonb
|
||||||
|
null;
|
||||||
3110
package-lock.json
generated
3110
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -18,25 +18,27 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.997.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.1009.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.997.0",
|
"@aws-sdk/client-elasticache": "^3.1009.0",
|
||||||
"@aws-sdk/client-s3": "^3.997.0",
|
"@aws-sdk/client-s3": "^3.1009.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.997.0",
|
"@aws-sdk/client-secrets-manager": "^3.1009.0",
|
||||||
"@aws-sdk/client-ses": "^3.997.0",
|
"@aws-sdk/client-ses": "^3.1009.0",
|
||||||
"@aws-sdk/client-sqs": "^3.997.0",
|
"@aws-sdk/client-sqs": "^3.1009.0",
|
||||||
"@aws-sdk/client-textract": "^3.997.0",
|
"@aws-sdk/client-textract": "^3.1009.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.12",
|
"@aws-sdk/credential-provider-node": "^3.972.21",
|
||||||
"@aws-sdk/lib-storage": "^3.997.0",
|
"@aws-sdk/lib-storage": "^3.1009.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.997.0",
|
"@aws-sdk/s3-request-presigner": "^3.1009.0",
|
||||||
|
"@documenso/sdk-typescript": "^0.8.0",
|
||||||
|
"@jsreport/nodejs-client": "^4.1.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.13.5",
|
"axios": "^1.13.6",
|
||||||
"axios-curlirize": "^2.0.0",
|
"axios-curlirize": "^2.0.0",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.70.1",
|
"bullmq": "^5.71.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cloudinary": "^2.9.0",
|
"cloudinary": "^2.9.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -46,20 +48,20 @@
|
|||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"fast-xml-parser": "^5.4.1",
|
"fast-xml-parser": "^5.5.6",
|
||||||
"firebase-admin": "^13.6.1",
|
"firebase-admin": "^13.7.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"graphql": "^16.13.0",
|
"graphql": "^16.13.1",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^7.4.0",
|
||||||
"intuit-oauth": "^4.2.2",
|
"intuit-oauth": "^4.2.2",
|
||||||
"ioredis": "^5.9.3",
|
"ioredis": "^5.10.0",
|
||||||
"json-2-csv": "^5.5.10",
|
"json-2-csv": "^5.5.10",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"juice": "^11.1.1",
|
"juice": "^11.1.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.1.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
@@ -69,15 +71,15 @@
|
|||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"rimraf": "^6.1.3",
|
"rimraf": "^6.1.3",
|
||||||
"skia-canvas": "^3.0.8",
|
"skia-canvas": "^3.0.8",
|
||||||
"soap": "^1.7.1",
|
"soap": "^1.8.0",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-adapter": "^2.5.6",
|
"socket.io-adapter": "^2.5.6",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.12.2",
|
"twilio": "^5.13.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.19.0",
|
"winston": "^3.19.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
"xml-formatter": "^3.6.7",
|
"xml-formatter": "^3.7.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^4.0.3",
|
"xmlbuilder2": "^4.0.3",
|
||||||
"yazl": "^3.3.1"
|
"yazl": "^3.3.1"
|
||||||
@@ -86,11 +88,11 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.4.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.2.2",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ const applyRoutes = ({ app }) => {
|
|||||||
app.use("/ai", require("./server/routes/aiRoutes"));
|
app.use("/ai", require("./server/routes/aiRoutes"));
|
||||||
|
|
||||||
app.use("/chatter", require("./server/routes/chatterRoutes"));
|
app.use("/chatter", require("./server/routes/chatterRoutes"));
|
||||||
|
app.use("/esign", require("./server/routes/esignRoutes"));
|
||||||
|
|
||||||
// Default route for forbidden access
|
// Default route for forbidden access
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
|
|||||||
@@ -130,12 +130,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(
|
||||||
|
qbo_realmId,
|
||||||
|
"query",
|
||||||
|
`select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'`
|
||||||
|
);
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: url,
|
||||||
qbo_realmId,
|
|
||||||
"query",
|
|
||||||
`select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'`
|
|
||||||
),
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -150,6 +151,11 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
bodyshopid: bill.job.shopid,
|
bodyshopid: bill.job.shopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-payables-query", "DEBUG", req.user.email, null, {
|
||||||
|
method: "QueryVendorRecord",
|
||||||
|
call: url,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
return result.json?.QueryResponse?.Vendor?.[0];
|
return result.json?.QueryResponse?.Vendor?.[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -167,8 +173,9 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
DisplayName: StandardizeName(bill.vendor.name)
|
DisplayName: StandardizeName(bill.vendor.name)
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "vendor");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "vendor"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -184,6 +191,12 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
bodyshopid: bill.job.shopid,
|
bodyshopid: bill.job.shopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, {
|
||||||
|
method: "InsertVendorRecord",
|
||||||
|
call: url,
|
||||||
|
Vendor: Vendor,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
if (result.status >= 400) {
|
if (result.status >= 400) {
|
||||||
throw new Error(JSON.stringify(result.json.Fault));
|
throw new Error(JSON.stringify(result.json.Fault));
|
||||||
@@ -274,11 +287,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
VendorRef: {
|
VendorRef: {
|
||||||
value: vendor.Id
|
value: vendor.Id
|
||||||
},
|
},
|
||||||
...(vendor.TermRef && !bill.is_credit_memo && {
|
...(vendor.TermRef &&
|
||||||
SalesTermRef: {
|
!bill.is_credit_memo && {
|
||||||
value: vendor.TermRef.value
|
SalesTermRef: {
|
||||||
}
|
value: vendor.TermRef.value
|
||||||
}),
|
}
|
||||||
|
}),
|
||||||
TxnDate: moment(bill.date)
|
TxnDate: moment(bill.date)
|
||||||
//.tz(bill.job.bodyshop.timezone)
|
//.tz(bill.job.bodyshop.timezone)
|
||||||
.format("YYYY-MM-DD"),
|
.format("YYYY-MM-DD"),
|
||||||
@@ -318,8 +332,9 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
[logKey]: logValue
|
[logKey]: logValue
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -335,6 +350,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
bodyshopid: bill.job.shopid,
|
bodyshopid: bill.job.shopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-payables-insert", "DEBUG", req.user.email, null, {
|
||||||
|
method: "InsertBill",
|
||||||
|
call: url,
|
||||||
|
postingObj: bill.is_credit_memo ? VendorCredit : billQbo,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
if (result.status >= 400) {
|
if (result.status >= 400) {
|
||||||
throw new Error(JSON.stringify(result.json.Fault));
|
throw new Error(JSON.stringify(result.json.Fault));
|
||||||
|
|||||||
@@ -82,14 +82,7 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
if (isThreeTier || (!isThreeTier && twoTierPref === "name")) {
|
if (isThreeTier || (!isThreeTier && twoTierPref === "name")) {
|
||||||
//Insert the name/owner and account for whether the source should be the ins co in 3 tier..
|
//Insert the name/owner and account for whether the source should be the ins co in 3 tier..
|
||||||
ownerCustomerTier = await QueryOwner(
|
ownerCustomerTier = await QueryOwner(oauthClient, qbo_realmId, req, payment.job, insCoCustomerTier);
|
||||||
oauthClient,
|
|
||||||
qbo_realmId,
|
|
||||||
req,
|
|
||||||
payment.job,
|
|
||||||
isThreeTier,
|
|
||||||
insCoCustomerTier
|
|
||||||
);
|
|
||||||
//Query for the owner itself.
|
//Query for the owner itself.
|
||||||
if (!ownerCustomerTier) {
|
if (!ownerCustomerTier) {
|
||||||
ownerCustomerTier = await InsertOwner(
|
ownerCustomerTier = await InsertOwner(
|
||||||
@@ -229,8 +222,9 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef)
|
|||||||
paymentQbo
|
paymentQbo
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "payment");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "payment"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -246,6 +240,12 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef)
|
|||||||
bodyshopid: payment.job.shopid,
|
bodyshopid: payment.job.shopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, {
|
||||||
|
method: "InsertPayment",
|
||||||
|
call: url,
|
||||||
|
paymentQbo: paymentQbo,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
if (result.status >= 400) {
|
if (result.status >= 400) {
|
||||||
throw new Error(JSON.stringify(result.json.Fault));
|
throw new Error(JSON.stringify(result.json.Fault));
|
||||||
@@ -428,8 +428,9 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
paymentQbo
|
paymentQbo
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "creditmemo");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "creditmemo"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -445,6 +446,12 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
bodyshopid: req.user.bodyshopid,
|
bodyshopid: req.user.bodyshopid,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-metadata-query", "DEBUG", req.user.email, null, {
|
||||||
|
method: "InsertCreditMemo",
|
||||||
|
call: url,
|
||||||
|
paymentQbo: paymentQbo,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
if (result.status >= 400) {
|
if (result.status >= 400) {
|
||||||
throw new Error(JSON.stringify(result.json.Fault));
|
throw new Error(JSON.stringify(result.json.Fault));
|
||||||
|
|||||||
@@ -213,12 +213,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(
|
||||||
|
qbo_realmId,
|
||||||
|
"query",
|
||||||
|
`select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true`
|
||||||
|
);
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: url,
|
||||||
qbo_realmId,
|
|
||||||
"query",
|
|
||||||
`select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true`
|
|
||||||
),
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -233,6 +234,11 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "QueryInsuranceCo",
|
||||||
|
call: url,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
return result.json?.QueryResponse?.Customer?.[0];
|
return result.json?.QueryResponse?.Customer?.[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -266,8 +272,9 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "customer");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "customer"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -283,6 +290,12 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "InsertInsuranceCo",
|
||||||
|
call: url,
|
||||||
|
customerObj: Customer,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
return result.json?.Customer;
|
return result.json?.Customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -298,12 +311,13 @@ exports.InsertInsuranceCo = InsertInsuranceCo;
|
|||||||
|
|
||||||
async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||||
const ownerName = generateOwnerTier(job, true, null);
|
const ownerName = generateOwnerTier(job, true, null);
|
||||||
|
const url = urlBuilder(
|
||||||
|
qbo_realmId,
|
||||||
|
"query",
|
||||||
|
`select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true`
|
||||||
|
);
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: url,
|
||||||
qbo_realmId,
|
|
||||||
"query",
|
|
||||||
`select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true`
|
|
||||||
),
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -318,6 +332,11 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "QueryOwner",
|
||||||
|
call: url,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
return result.json?.QueryResponse?.Customer?.find((x) => x.ParentRef?.value === parentTierRef?.Id);
|
return result.json?.QueryResponse?.Customer?.find((x) => x.ParentRef?.value === parentTierRef?.Id);
|
||||||
}
|
}
|
||||||
@@ -347,8 +366,9 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
|||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "customer");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "customer"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -364,6 +384,12 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "InsertOwner",
|
||||||
|
call: url,
|
||||||
|
customerObj: Customer,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
return result.json?.Customer;
|
return result.json?.Customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -378,12 +404,13 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
|||||||
exports.InsertOwner = InsertOwner;
|
exports.InsertOwner = InsertOwner;
|
||||||
|
|
||||||
async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||||
|
const url = urlBuilder(
|
||||||
|
qbo_realmId,
|
||||||
|
"query",
|
||||||
|
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
|
||||||
|
);
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: url,
|
||||||
qbo_realmId,
|
|
||||||
"query",
|
|
||||||
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
|
|
||||||
),
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -398,6 +425,11 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "QueryJob",
|
||||||
|
call: url,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
const customers = result.json?.QueryResponse?.Customer;
|
const customers = result.json?.QueryResponse?.Customer;
|
||||||
return customers && (parentTierRef ? customers.find((x) => x.ParentRef.value === parentTierRef.Id) : customers[0]);
|
return customers && (parentTierRef ? customers.find((x) => x.ParentRef.value === parentTierRef.Id) : customers[0]);
|
||||||
@@ -423,8 +455,9 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
const url = urlBuilder(qbo_realmId, "customer");
|
||||||
const result = await oauthClient.makeApiCall({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "customer"),
|
url: url,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -440,6 +473,12 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
});
|
});
|
||||||
|
logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, {
|
||||||
|
method: "InsertJob",
|
||||||
|
call: url,
|
||||||
|
customerObj: Customer,
|
||||||
|
result: result.json
|
||||||
|
});
|
||||||
|
|
||||||
if (result.status >= 400) {
|
if (result.status >= 400) {
|
||||||
throw new Error(JSON.stringify(result.json.Fault));
|
throw new Error(JSON.stringify(result.json.Fault));
|
||||||
|
|||||||
@@ -66,7 +66,12 @@ exports.default = async function ReloadCdkMakes(req, res) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, {
|
logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, {
|
||||||
cdk_dealerid,
|
cdk_dealerid,
|
||||||
error
|
error: {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
name: error?.name,
|
||||||
|
code: error?.code
|
||||||
|
}
|
||||||
});
|
});
|
||||||
res.status(500).json(error);
|
res.status(500).json(error);
|
||||||
}
|
}
|
||||||
@@ -105,7 +110,12 @@ async function GetCdkMakes(req, cdk_dealerid) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, {
|
logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, {
|
||||||
cdk_dealerid,
|
cdk_dealerid,
|
||||||
error
|
error: {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
name: error?.name,
|
||||||
|
code: error?.code
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
@@ -141,7 +151,12 @@ async function GetFortellisMakes(req, cdk_dealerid) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, {
|
logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, {
|
||||||
cdk_dealerid,
|
cdk_dealerid,
|
||||||
error
|
error: {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
name: error?.name,
|
||||||
|
code: error?.code
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
|
|||||||
304
server/esign/esign-new.js
Normal file
304
server/esign/esign-new.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
|
||||||
|
const { Documenso } = require("@documenso/sdk-typescript");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { jsrAuthString } = require("../utils/utils");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const DOCUMENSO_API_KEY = "api_asojim0czruv13ud";//Done on a by team basis,
|
||||||
|
const documenso = new Documenso({
|
||||||
|
apiKey: DOCUMENSO_API_KEY,//Done on a by team basis,
|
||||||
|
serverURL: "https://stg-app.documenso.com/api/v2",
|
||||||
|
});
|
||||||
|
const JSR_SERVER = "https://reports.test.imex.online";
|
||||||
|
const jsreport = require("@jsreport/nodejs-client");
|
||||||
|
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
||||||
|
|
||||||
|
|
||||||
|
async function distributeDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
const { documentId } = req.body;
|
||||||
|
const distributeResult = await documenso.documents.distribute({
|
||||||
|
documentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||||
|
obj: {
|
||||||
|
jobid: req.body.jobid,
|
||||||
|
bodyshopid: req.body.bodyshopid,
|
||||||
|
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
|
||||||
|
useremail: req.user?.email,
|
||||||
|
type: 'esig-distribute'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ success: true, distributeResult });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error distributing document:", error?.data);
|
||||||
|
logger.log(`esig-distribute-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "An error occurred while distributing the document." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const { documentId } = req.body;
|
||||||
|
//TODO: This needs to be hardened to prevent deleting other people's documents, completed ones, etc.
|
||||||
|
const deleteResult = await documenso.documents.delete({
|
||||||
|
documentId
|
||||||
|
});
|
||||||
|
res.json({ success: true, deleteResult });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting document:", error?.data);
|
||||||
|
logger.log(`esig-delete-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "An error occurred while deleting the document." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newEsignDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
const { bodyshop } = req.body
|
||||||
|
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
|
||||||
|
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
||||||
|
|
||||||
|
|
||||||
|
//Get the Job data.
|
||||||
|
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid: req.body.jobid });
|
||||||
|
|
||||||
|
const createDocumentResponse = await documenso.documents.create({
|
||||||
|
payload: {
|
||||||
|
title: esigData?.title,
|
||||||
|
externalId: `${req.body.jobid}|${req.user?.email}`, //Have to pass the uploaded by later on. Limited to 255 chars.
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: "patrick@imexsystems.ca",//jobData.ownr_ea,
|
||||||
|
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
|
||||||
|
role: "SIGNER",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
timezone: bodyshop.timezone,
|
||||||
|
dateFormat: "MM/dd/yyyy hh:mm a",
|
||||||
|
language: "en",
|
||||||
|
subject: esigData?.subject,
|
||||||
|
message: esigData?.message,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
file: fileBlob
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentResult = await documenso.documents.get({
|
||||||
|
documentId: createDocumentResponse.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (esigData?.fields && esigData.fields.length > 0) {
|
||||||
|
try {
|
||||||
|
await documenso.envelopes.fields.createMany({
|
||||||
|
envelopeId: createDocumentResponse.envelopeId,
|
||||||
|
data: esigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-new-fields-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
|
||||||
|
|
||||||
|
//add to job audit trail.
|
||||||
|
|
||||||
|
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||||
|
obj: {
|
||||||
|
jobid: req.body.jobid,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
operation: `Esignature document created. Subject: ${esigData?.subject || "No subject"}, Message: ${esigData?.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
|
||||||
|
useremail: req.user?.email,
|
||||||
|
type: 'esig-create'
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ token: presignToken.token, documentId: createDocumentResponse.id, envelopeId: createDocumentResponse.envelopeId });
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "An error occurred while creating the e-sign document." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function RenderTemplate({ req }) {
|
||||||
|
//TODO Refactor to pull
|
||||||
|
const jsrAuth = jsrAuthString()
|
||||||
|
|
||||||
|
const jsreportClient = new jsreport("https://reports.test.imex.online", process.env.JSR_USER, process.env.JSR_PASSWORD);
|
||||||
|
const { templateObject, bodyshop } = req.body;
|
||||||
|
let { contextData, useShopSpecificTemplate, shopSpecificFolder, esigData } = await fetchContextData({ templateObject, jsrAuth, req });
|
||||||
|
|
||||||
|
const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name];
|
||||||
|
let reportRequest = {
|
||||||
|
template: {
|
||||||
|
name: useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${templateObject.name}` : `/${templateObject.name}`,
|
||||||
|
|
||||||
|
recipe: "chrome-pdf",
|
||||||
|
...(!ignoreCustomMargins && {
|
||||||
|
chrome: {
|
||||||
|
marginTop:
|
||||||
|
bodyshop.logo_img_path &&
|
||||||
|
bodyshop.logo_img_path.headerMargin &&
|
||||||
|
bodyshop.logo_img_path.headerMargin > 36
|
||||||
|
? bodyshop.logo_img_path.headerMargin
|
||||||
|
: "36px",
|
||||||
|
marginBottom:
|
||||||
|
bodyshop.logo_img_path &&
|
||||||
|
bodyshop.logo_img_path.footerMargin &&
|
||||||
|
bodyshop.logo_img_path.footerMargin > 50
|
||||||
|
? bodyshop.logo_img_path.footerMargin
|
||||||
|
: "50px"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...contextData,
|
||||||
|
...templateObject.variables,
|
||||||
|
...templateObject.context,
|
||||||
|
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
|
||||||
|
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
filters: templateObject?.filters,
|
||||||
|
sorters: templateObject?.sorters,
|
||||||
|
offset: bodyshop.timezone, //dayjs().utcOffset(),
|
||||||
|
defaultSorters: templateObject?.defaultSorters
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const render = await jsreportClient.render(reportRequest);
|
||||||
|
|
||||||
|
//Check render object and download. It should be the PDF?
|
||||||
|
const pdfBuffer = await render.body()
|
||||||
|
return { pdf: pdfBuffer, esigData }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
|
||||||
|
const { bodyshop } = req.body
|
||||||
|
|
||||||
|
|
||||||
|
const folders = await axios.get(`${JSR_SERVER}/odata/folders`, {
|
||||||
|
headers: { Authorization: jsrAuth }
|
||||||
|
});
|
||||||
|
const shopSpecificFolder = folders.data.value.find((f) => f.name === bodyshop.imexshopid);
|
||||||
|
|
||||||
|
const jsReportQueries = await axios.get(
|
||||||
|
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||||
|
{ headers: { Authorization: jsrAuth } }
|
||||||
|
);
|
||||||
|
const jsReportEsig = await axios.get(
|
||||||
|
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
|
||||||
|
{ headers: { Authorization: jsrAuth } }
|
||||||
|
);
|
||||||
|
|
||||||
|
let templateQueryToExecute;
|
||||||
|
let esigData;
|
||||||
|
let useShopSpecificTemplate = false;
|
||||||
|
// let shopSpecificTemplate;
|
||||||
|
|
||||||
|
if (shopSpecificFolder) {
|
||||||
|
let shopSpecificTemplate = jsReportQueries.data.value.find(
|
||||||
|
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||||
|
);
|
||||||
|
if (shopSpecificTemplate) {
|
||||||
|
useShopSpecificTemplate = true;
|
||||||
|
templateQueryToExecute = atob(shopSpecificTemplate.content);
|
||||||
|
}
|
||||||
|
let shopSpecificEsig = jsReportEsig.data.value.find(
|
||||||
|
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||||
|
);
|
||||||
|
if (shopSpecificEsig) {
|
||||||
|
esigData = (atob(shopSpecificEsig.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateQueryToExecute) {
|
||||||
|
const generalTemplate = jsReportQueries.data.value.find((f) => !f.folder);
|
||||||
|
useShopSpecificTemplate = false;
|
||||||
|
templateQueryToExecute = atob(generalTemplate.content);
|
||||||
|
}
|
||||||
|
if (!esigData) {
|
||||||
|
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
|
||||||
|
useShopSpecificTemplate = false;
|
||||||
|
if (generalTemplate && generalTemplate.content) {
|
||||||
|
esigData = atob(generalTemplate?.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
|
||||||
|
// In the print center, we will never have sorters or filters.
|
||||||
|
// We have no template filters or sorters, so we can just execute the query and return the data
|
||||||
|
// if (!hasFilters && !hasSorters && !hasDefaultSorters) {
|
||||||
|
let contextData = {};
|
||||||
|
if (templateQueryToExecute) {
|
||||||
|
const data = await client.request(
|
||||||
|
templateQueryToExecute,
|
||||||
|
templateObject.variables,
|
||||||
|
);
|
||||||
|
contextData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedEsigData
|
||||||
|
try {
|
||||||
|
parsedEsigData = esigData ? JSON.parse(esigData) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error parsing esig data", error);
|
||||||
|
parsedEsigData = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextData,
|
||||||
|
useShopSpecificTemplate,
|
||||||
|
shopSpecificFolder,
|
||||||
|
esigData: parsedEsigData
|
||||||
|
};
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
newEsignDocument,
|
||||||
|
distributeDocument,
|
||||||
|
deleteDocument
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const sample_esig_for_jsr = {
|
||||||
|
// "fields": [
|
||||||
|
// {
|
||||||
|
// "placeholder": "[[signature]]",
|
||||||
|
// "type": "SIGNATURE"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "placeholder": "[[date]]",
|
||||||
|
// "type": "DATE"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "subject": "CASL Auth Set in JSR",
|
||||||
|
// "message": "CASL Message set in JSR",
|
||||||
|
// "title": "CASL Title set in JSR"
|
||||||
|
// }
|
||||||
393
server/esign/webhook.js
Normal file
393
server/esign/webhook.js
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
|
||||||
|
const { Documenso } = require("@documenso/sdk-typescript");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const { QUERY_META_FOR_ESIG_COMPLETION, INSERT_ESIGNATURE_DOCUMENT, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
||||||
|
const { uploadFileBuffer } = require("../media/imgproxy-media");
|
||||||
|
const client = require('../graphql-client/graphql-client').client;
|
||||||
|
const documenso = new Documenso({
|
||||||
|
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
|
||||||
|
serverURL: "https://stg-app.documenso.com/api/v2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const webhookTypeEnums = {
|
||||||
|
DOCUMENT_CREATED: "DOCUMENT_CREATED",
|
||||||
|
DOCUMENT_SENT: "DOCUMENT_SENT",
|
||||||
|
DOCUMENT_COMPLETED: "DOCUMENT_COMPLETED",
|
||||||
|
DOCUMENT_REJECTED: "DOCUMENT_REJECTED",
|
||||||
|
DOCUMENT_CANCELLED: "DOCUMENT_CANCELLED",
|
||||||
|
DOCUMENT_OPENED: "DOCUMENT_OPENED",
|
||||||
|
DOCUMENT_SIGNED: "DOCUMENT_SIGNED",
|
||||||
|
}
|
||||||
|
|
||||||
|
async function esignWebhook(req, res) {
|
||||||
|
console.log("Esign Webhook Received:", req.body);
|
||||||
|
try {
|
||||||
|
const message = req.body
|
||||||
|
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
|
||||||
|
event: message.event,
|
||||||
|
body: message
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (message.event) {
|
||||||
|
case webhookTypeEnums.DOCUMENT_CREATED:
|
||||||
|
//This is largely a throwaway event we know it was created.
|
||||||
|
console.log("Document created event received. Document ID:", message.payload.documentId);
|
||||||
|
// Here you can add any additional processing you want to do when a document is created
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_COMPLETED:
|
||||||
|
console.log("Document completed event received. Document ID:", message.payload.documentId);
|
||||||
|
await handleDocumentCompleted(message.payload);
|
||||||
|
// Here you can add any additional processing you want to do when a document is completed
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_SIGNED:
|
||||||
|
console.log("Document signed event received. Document ID:", message.payload.documentId);
|
||||||
|
// Here you can add any additional processing you want to do when a document is signed
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`Unhandled event type: ${message.event}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// const result = await documenso.documents.download({
|
||||||
|
// documentId: req.body.payload.id,
|
||||||
|
// });
|
||||||
|
// result.resultingBuffer = Buffer.from(result.resultingArrayBuffer);
|
||||||
|
// // Save the document to a file for testing purposes
|
||||||
|
// const downloadsDir = path.join(__dirname, '../downloads');
|
||||||
|
// if (!fs.existsSync(downloadsDir)) {
|
||||||
|
// fs.mkdirSync(downloadsDir, { recursive: true });
|
||||||
|
// }
|
||||||
|
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
|
||||||
|
// fs.writeFileSync(filePath, result.resultingBuffer);
|
||||||
|
|
||||||
|
// console.log(result)
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
// const downloadsDir = path.join(__dirname, '../downloads');
|
||||||
|
// if (!fs.existsSync(downloadsDir)) {
|
||||||
|
// fs.mkdirSync(downloadsDir, { recursive: true });
|
||||||
|
// }
|
||||||
|
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
|
||||||
|
// fs.writeFileSync(filePath, Buffer.from(err.body));
|
||||||
|
// console.error("Error handling esign webhook:", err);
|
||||||
|
res.sendStatus(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDocumentCompleted(payload = sampleComplete) {
|
||||||
|
|
||||||
|
|
||||||
|
//Check if the bodyshop is on image proxy or not
|
||||||
|
try {
|
||||||
|
//Split the external id to get the uploaded user.
|
||||||
|
const [jobid, uploaded_by] = payload.externalId.split("|");
|
||||||
|
|
||||||
|
if (!jobid || !uploaded_by) {
|
||||||
|
throw new Error(`Invalid externalId format. Expected "jobid|uploaded_by", got "${payload.externalId}"`);
|
||||||
|
}
|
||||||
|
const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, {
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
const document = await documenso.document.documentDownload({
|
||||||
|
documentId: payload.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(document.downloadUrl);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
|
||||||
|
let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`;
|
||||||
|
|
||||||
|
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
|
||||||
|
//LMS not yet implemented.
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//S3 Upload
|
||||||
|
const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" });
|
||||||
|
if (!uploadResult.success) {
|
||||||
|
logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", {
|
||||||
|
message: uploadResult.message,
|
||||||
|
stack: uploadResult.stack,
|
||||||
|
jobid: jobid,
|
||||||
|
documentId: payload.id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
|
||||||
|
jobid: jobid,
|
||||||
|
documentId: payload.id,
|
||||||
|
s3Key: key,
|
||||||
|
bucket: uploadResult.bucket
|
||||||
|
});
|
||||||
|
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||||
|
obj: {
|
||||||
|
jobid: jobs_by_pk.id,
|
||||||
|
bodyshopid: jobs_by_pk.bodyshop.id,
|
||||||
|
operation: `Esignature document with title ${payload.title} (ID: ${payload.documentMeta.id}) has been completed.`,
|
||||||
|
useremail: uploaded_by,
|
||||||
|
type: 'esig-complete'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//insert the document record with the s3 key and bucket info.
|
||||||
|
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
|
||||||
|
docInput: {
|
||||||
|
jobid: jobs_by_pk.id,
|
||||||
|
uploaded_by: uploaded_by,
|
||||||
|
key,
|
||||||
|
type: "application/pdf",
|
||||||
|
extension: "pdf",
|
||||||
|
bodyshopid: jobs_by_pk.bodyshop.id,
|
||||||
|
size: buffer.length,
|
||||||
|
takenat: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
esignWebhook
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sampleComplete = {
|
||||||
|
"id": 10929,
|
||||||
|
"title": "CASL Title set in JSR",
|
||||||
|
"source": "DOCUMENT",
|
||||||
|
"status": "COMPLETED",
|
||||||
|
"teamId": 742,
|
||||||
|
"userId": 654,
|
||||||
|
"Recipient": [
|
||||||
|
{
|
||||||
|
"id": 24997,
|
||||||
|
"name": "James Tschetter",
|
||||||
|
"role": "SIGNER",
|
||||||
|
"email": "patrick@imexsystems.ca",
|
||||||
|
"token": "uMom0GwL29NBqMfohGpUE",
|
||||||
|
"signedAt": "2026-02-27T22:11:52.835Z",
|
||||||
|
"expiresAt": "2026-05-28T22:10:48.991Z",
|
||||||
|
"documentId": 10929,
|
||||||
|
"readStatus": "OPENED",
|
||||||
|
"sendStatus": "SENT",
|
||||||
|
"templateId": null,
|
||||||
|
"authOptions": {
|
||||||
|
"accessAuth": [],
|
||||||
|
"actionAuth": []
|
||||||
|
},
|
||||||
|
"signingOrder": null,
|
||||||
|
"signingStatus": "SIGNED",
|
||||||
|
"rejectionReason": null,
|
||||||
|
"documentDeletedAt": null,
|
||||||
|
"expirationNotifiedAt": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"createdAt": "2026-02-27T22:10:10.580Z",
|
||||||
|
"deletedAt": null,
|
||||||
|
"updatedAt": "2026-02-27T22:11:57.753Z",
|
||||||
|
"externalId": null,
|
||||||
|
"formValues": null,
|
||||||
|
"recipients": [
|
||||||
|
{
|
||||||
|
"id": 24997,
|
||||||
|
"name": "James Tschetter",
|
||||||
|
"role": "SIGNER",
|
||||||
|
"email": "patrick@imexsystems.ca",
|
||||||
|
"token": "uMom0GwL29NBqMfohGpUE",
|
||||||
|
"signedAt": "2026-02-27T22:11:52.835Z",
|
||||||
|
"expiresAt": "2026-05-28T22:10:48.991Z",
|
||||||
|
"documentId": 10929,
|
||||||
|
"readStatus": "OPENED",
|
||||||
|
"sendStatus": "SENT",
|
||||||
|
"templateId": null,
|
||||||
|
"authOptions": {
|
||||||
|
"accessAuth": [],
|
||||||
|
"actionAuth": []
|
||||||
|
},
|
||||||
|
"signingOrder": null,
|
||||||
|
"signingStatus": "SIGNED",
|
||||||
|
"rejectionReason": null,
|
||||||
|
"documentDeletedAt": null,
|
||||||
|
"expirationNotifiedAt": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"templateId": null,
|
||||||
|
"visibility": "EVERYONE",
|
||||||
|
"authOptions": {
|
||||||
|
"globalAccessAuth": [],
|
||||||
|
"globalActionAuth": []
|
||||||
|
},
|
||||||
|
"completedAt": "2026-02-27T22:11:57.752Z",
|
||||||
|
"documentMeta": {
|
||||||
|
"id": "cmm5g3y7u00ecad1sv3ague1w",
|
||||||
|
"message": "CASL Message set in JSR",
|
||||||
|
"subject": "CASL Auth Set in JSR",
|
||||||
|
"language": "en",
|
||||||
|
"timezone": "Etc/UTC",
|
||||||
|
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||||
|
"redirectUrl": null,
|
||||||
|
"signingOrder": "PARALLEL",
|
||||||
|
"emailSettings": {
|
||||||
|
"documentDeleted": true,
|
||||||
|
"documentPending": true,
|
||||||
|
"recipientSigned": true,
|
||||||
|
"recipientRemoved": true,
|
||||||
|
"documentCompleted": true,
|
||||||
|
"ownerDocumentCompleted": true,
|
||||||
|
"recipientSigningRequest": true
|
||||||
|
},
|
||||||
|
"distributionMethod": "EMAIL",
|
||||||
|
"drawSignatureEnabled": true,
|
||||||
|
"typedSignatureEnabled": true,
|
||||||
|
"allowDictateNextSigner": false,
|
||||||
|
"uploadSignatureEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const sampleBody = {
|
||||||
|
// event: "DOCUMENT_COMPLETED",
|
||||||
|
// payload: {
|
||||||
|
// Recipient: [
|
||||||
|
// {
|
||||||
|
// authOptions: {
|
||||||
|
// accessAuth: [
|
||||||
|
// ],
|
||||||
|
// actionAuth: [
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// documentDeletedAt: null,
|
||||||
|
// documentId: 9827,
|
||||||
|
// email: "patrick@imexsystems.ca",
|
||||||
|
// expired: null,
|
||||||
|
// id: 13311,
|
||||||
|
// name: "Customer Fullname",
|
||||||
|
// readStatus: "OPENED",
|
||||||
|
// rejectionReason: null,
|
||||||
|
// role: "SIGNER",
|
||||||
|
// sendStatus: "SENT",
|
||||||
|
// signedAt: "2026-01-30T18:29:12.648Z",
|
||||||
|
// signingOrder: null,
|
||||||
|
// signingStatus: "SIGNED",
|
||||||
|
// templateId: null,
|
||||||
|
// token: "uiEWIsXUPTbWHd7QedVgt",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// authOptions: {
|
||||||
|
// globalAccessAuth: [
|
||||||
|
// ],
|
||||||
|
// globalActionAuth: [
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// completedAt: "2026-01-30T18:29:16.279Z",
|
||||||
|
// createdAt: "2026-01-30T18:28:48.861Z",
|
||||||
|
// deletedAt: null,
|
||||||
|
// documentMeta: {
|
||||||
|
// allowDictateNextSigner: false,
|
||||||
|
// dateFormat: "yyyy-MM-dd hh:mm a",
|
||||||
|
// distributionMethod: "EMAIL",
|
||||||
|
// drawSignatureEnabled: true,
|
||||||
|
// emailSettings: {
|
||||||
|
// documentCompleted: true,
|
||||||
|
// documentDeleted: true,
|
||||||
|
// documentPending: true,
|
||||||
|
// ownerDocumentCompleted: true,
|
||||||
|
// recipientRemoved: false,
|
||||||
|
// recipientSigned: true,
|
||||||
|
// recipientSigningRequest: true,
|
||||||
|
// },
|
||||||
|
// id: "cml17vfb200qjad1t2spxnc1n",
|
||||||
|
// language: "en",
|
||||||
|
// message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ",
|
||||||
|
// redirectUrl: null,
|
||||||
|
// signingOrder: "PARALLEL",
|
||||||
|
// subject: "Repair Authorization for ABC Collision",
|
||||||
|
// timezone: "Etc/UTC",
|
||||||
|
// typedSignatureEnabled: true,
|
||||||
|
// uploadSignatureEnabled: true,
|
||||||
|
// },
|
||||||
|
// externalId: null,
|
||||||
|
// formValues: null,
|
||||||
|
// id: 9827,
|
||||||
|
// recipients: [
|
||||||
|
// {
|
||||||
|
// authOptions: {
|
||||||
|
// accessAuth: [
|
||||||
|
// ],
|
||||||
|
// actionAuth: [
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// documentDeletedAt: null,
|
||||||
|
// documentId: 9827,
|
||||||
|
// email: "patrick@imexsystems.ca",
|
||||||
|
// expired: null,
|
||||||
|
// id: 13311,
|
||||||
|
// name: "Customer Fullname",
|
||||||
|
// readStatus: "OPENED",
|
||||||
|
// rejectionReason: null,
|
||||||
|
// role: "SIGNER",
|
||||||
|
// sendStatus: "SENT",
|
||||||
|
// signedAt: "2026-01-30T18:29:12.648Z",
|
||||||
|
// signingOrder: null,
|
||||||
|
// signingStatus: "SIGNED",
|
||||||
|
// templateId: null,
|
||||||
|
// token: "uiEWIsXUPTbWHd7QedVgt",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// source: "DOCUMENT",
|
||||||
|
// status: "COMPLETED",
|
||||||
|
// teamId: 742,
|
||||||
|
// templateId: null,
|
||||||
|
// title: "Repair Authorization - 1/30/2026, 6:28:48 PM",
|
||||||
|
// updatedAt: "2026-01-30T18:29:16.280Z",
|
||||||
|
// userId: 654,
|
||||||
|
// visibility: "EVERYONE",
|
||||||
|
// },
|
||||||
|
// createdAt: "2026-01-30T18:29:18.504Z",
|
||||||
|
// webhookEndpoint: "https://dev.patrickfic.com/esign/webhook",
|
||||||
|
// }
|
||||||
|
|
||||||
|
function replaceAccents(str) {
|
||||||
|
// Verifies if the String has accents and replace them
|
||||||
|
if (str.search(/[\xC0-\xFF]/g) > -1) {
|
||||||
|
str = str
|
||||||
|
.replace(/[\xC0-\xC5]/g, "A")
|
||||||
|
.replace(/[\xC6]/g, "AE")
|
||||||
|
.replace(/[\xC7]/g, "C")
|
||||||
|
.replace(/[\xC8-\xCB]/g, "E")
|
||||||
|
.replace(/[\xCC-\xCF]/g, "I")
|
||||||
|
.replace(/[\xD0]/g, "D")
|
||||||
|
.replace(/[\xD1]/g, "N")
|
||||||
|
.replace(/[\xD2-\xD6\xD8]/g, "O")
|
||||||
|
.replace(/[\xD9-\xDC]/g, "U")
|
||||||
|
.replace(/[\xDD]/g, "Y")
|
||||||
|
.replace(/[\xDE]/g, "P")
|
||||||
|
.replace(/[\xE0-\xE5]/g, "a")
|
||||||
|
.replace(/[\xE6]/g, "ae")
|
||||||
|
.replace(/[\xE7]/g, "c")
|
||||||
|
.replace(/[\xE8-\xEB]/g, "e")
|
||||||
|
.replace(/[\xEC-\xEF]/g, "i")
|
||||||
|
.replace(/[\xF1]/g, "n")
|
||||||
|
.replace(/[\xF2-\xF6\xF8]/g, "o")
|
||||||
|
.replace(/[\xF9-\xFC]/g, "u")
|
||||||
|
.replace(/[\xFE]/g, "p")
|
||||||
|
.replace(/[\xFD\xFF]/g, "y");
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
`Unexpected Status or Content-Type: Status 200 Content-Type application/pdf\nBody: %PDF-1.7\n%<25><><EFBFBD><EFBFBD>\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Names 74 0 R\n/Dests 75 0 R\n/Info 77 0 R\n/Lang (en-US)\n/Version /1.7\n>>\nendobj\n77 0 obj\n<<\n/Type /Info\n/CreationDate (D:20260227230617Z00'00')\n/Producer <FEFF007000640066002D006C006900620020002800680074007400700073003A002F002F006700690074006800750062002E0063006F006D002F0048006F007000640069006E0067002F007000640066002D006C006900620029>\n/ModDate (D:20260227231057Z)…<>5[<5B>><3E>Wu7<><37>V<EFBFBD><56><EFBFBD><EFBFBD><EFBFBD>Pw<50>WX<57>ܮJ'6NWg<57>vYϳ<><CFB3><EFBFBD><EFBFBD><EFBFBD>Щr<D0A9>\n\t+<2B>1<EFBFBD><10>m{휑<0C>hwb<><62><EFBFBD>8<EFBFBD><38>qy<>1e<31>)۱<>5m<35><6D><08><>MVM!<21>m<EFBFBD>[A<><41><10>{l<><6C>\t<EFBFBD>hia4<61><34>Tm<54><6D>8<><38>a<>e<EFBFBD>}<7D>߫<><DFAB><15>]MVpяG<D18F><47>֏<EFBFBD>jJ<"<22>A<EFBFBD>mO*<2A>P<EFBFBD><0B><><><7F><EFBFBD><EFBFBD>ѧЛ\nendstream\nendobj\n26 0 obj\n<<\n/Length 478/Filter /FlateDecode\n>>\nstream\nx<EFBFBD>MSK<EFBFBD>9<08><>)<29><>*<04>O<EFBFBD>i<EFBFBD><69>,<2C><>o <20><>kS%<25>$<EFBFBD><EFBFBD>hR\rS'<27>I<EFBFBD><49>~<7E><03><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T[/<2F>{<05>k<EFBFBD>FC#<23><>֛<><D69B><EFBFBD>;Ӏ<>[<5B>⫀m<E2AB80>|Q<1F><>\x1b<EFBFBD><16>><3E>R<><52><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD>E#<23>pI<70><49>._H<5F>ᆫt<E186AB>k<EFBFBD>D3p<33>I<EFBFBD><49><EFBFBD><EFBFBD><01>W2<57><32><EFBFBD>oJ0<4A>j<EFBFBD><6A><EFBFBD>j#<23><>!<21>$<EFBFBD><EFBFBD>-<2D><08><><EFBFBD><EFBFBD><EFBFBD>.Ϋ<><CEAB><EFBFBD>TI|8D<38>H<1C><>Y<EFBFBD><59>x<EFBFBD><78><EFBFBD><EFBFBD>1<EFBFBD>73%<25>u<EFBFBD>T<EFBFBD><54>Ӑ.rcb<63>x<EFBFBD><78>Dd6=<3D><>Oڏ1^<5E>-<2D>...and 252354 more chars`
|
||||||
95
server/esign/webhook.types.ts
Normal file
95
server/esign/webhook.types.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export type WebhookEventType =
|
||||||
|
| "DOCUMENT_CREATED"
|
||||||
|
| "DOCUMENT_SENT"
|
||||||
|
| "DOCUMENT_COMPLETED"
|
||||||
|
| "DOCUMENT_REJECTED"
|
||||||
|
| "DOCUMENT_CANCELLED"
|
||||||
|
| "DOCUMENT_OPENED"
|
||||||
|
| "DOCUMENT_SIGNED";
|
||||||
|
|
||||||
|
export interface AuthOptions {
|
||||||
|
accessAuth: unknown[];
|
||||||
|
actionAuth: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Recipient {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
email: string;
|
||||||
|
token?: string | null;
|
||||||
|
signedAt?: string | null;
|
||||||
|
expiresAt?: string | null;
|
||||||
|
documentId?: number;
|
||||||
|
readStatus?: string | null;
|
||||||
|
sendStatus?: string | null;
|
||||||
|
templateId?: number | null;
|
||||||
|
authOptions?: AuthOptions;
|
||||||
|
signingOrder?: number | null;
|
||||||
|
signingStatus?: string | null;
|
||||||
|
rejectionReason?: string | null;
|
||||||
|
documentDeletedAt?: string | null;
|
||||||
|
expirationNotifiedAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailSettings {
|
||||||
|
documentDeleted: boolean;
|
||||||
|
documentPending: boolean;
|
||||||
|
recipientSigned: boolean;
|
||||||
|
recipientRemoved: boolean;
|
||||||
|
documentCompleted: boolean;
|
||||||
|
ownerDocumentCompleted: boolean;
|
||||||
|
recipientSigningRequest: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentMeta {
|
||||||
|
id: string;
|
||||||
|
message?: string | null;
|
||||||
|
subject?: string | null;
|
||||||
|
language?: string | null;
|
||||||
|
timezone?: string | null;
|
||||||
|
dateFormat?: string | null;
|
||||||
|
redirectUrl?: string | null;
|
||||||
|
signingOrder?: string | null;
|
||||||
|
emailSettings?: EmailSettings;
|
||||||
|
distributionMethod?: string | null;
|
||||||
|
drawSignatureEnabled?: boolean;
|
||||||
|
typedSignatureEnabled?: boolean;
|
||||||
|
allowDictateNextSigner?: boolean;
|
||||||
|
uploadSignatureEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentAuthOptions {
|
||||||
|
globalAccessAuth: unknown[];
|
||||||
|
globalActionAuth: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentPayload {
|
||||||
|
id: number;
|
||||||
|
title?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
teamId?: number | null;
|
||||||
|
userId?: number | null;
|
||||||
|
Recipient?: Recipient[];
|
||||||
|
recipients?: Recipient[];
|
||||||
|
createdAt?: string | null;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
externalId?: string | null;
|
||||||
|
formValues?: unknown | null;
|
||||||
|
templateId?: number | null;
|
||||||
|
visibility?: string | null;
|
||||||
|
authOptions?: DocumentAuthOptions;
|
||||||
|
completedAt?: string | null;
|
||||||
|
documentMeta?: DocumentMeta | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebhookEventPayload {
|
||||||
|
event: WebhookEventType;
|
||||||
|
payload: DocumentPayload;
|
||||||
|
createdAt?: string | null;
|
||||||
|
webhookEndpoint?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebhookEventPayload;
|
||||||
@@ -959,7 +959,7 @@ async function UpdateDmsVehicle({ socket, redisHelpers, JobData, DMSVeh, DMSCust
|
|||||||
delete DMSVehToSend.inventoryAccount;
|
delete DMSVehToSend.inventoryAccount;
|
||||||
|
|
||||||
!DMSVehToSend.vehicle.engineNumber && delete DMSVehToSend.vehicle.engineNumber;
|
!DMSVehToSend.vehicle.engineNumber && delete DMSVehToSend.vehicle.engineNumber;
|
||||||
!DMSVehToSend.vehicle.saleClassValue && DMSVehToSend.vehicle.saleClassValue === "MISC";
|
!DMSVehToSend.vehicle.saleClassValue && (DMSVehToSend.vehicle.saleClassValue = "MISC");
|
||||||
!DMSVehToSend.vehicle.exteriorColor && delete DMSVehToSend.vehicle.exteriorColor;
|
!DMSVehToSend.vehicle.exteriorColor && delete DMSVehToSend.vehicle.exteriorColor;
|
||||||
|
|
||||||
const result = await MakeFortellisCall({
|
const result = await MakeFortellisCall({
|
||||||
|
|||||||
@@ -2253,18 +2253,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
|
|||||||
|
|
||||||
exports.INSERT_NEW_TRANSITION = (
|
exports.INSERT_NEW_TRANSITION = (
|
||||||
includeOldTransition
|
includeOldTransition
|
||||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
|
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||||
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
|
||||||
}) {
|
}) {
|
||||||
insert_transitions_one(object: $newTransition) {
|
insert_transitions_one(object: $newTransition) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
${
|
${includeOldTransition
|
||||||
includeOldTransition
|
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
|
||||||
affected_rows
|
affected_rows
|
||||||
}`
|
}`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -2463,6 +2461,8 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
|||||||
}
|
}
|
||||||
percentage
|
percentage
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2473,6 +2473,7 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
|||||||
productivehrs
|
productivehrs
|
||||||
actualhrs
|
actualhrs
|
||||||
ciecacode
|
ciecacode
|
||||||
|
payout_context
|
||||||
}
|
}
|
||||||
lbr_adjustments
|
lbr_adjustments
|
||||||
ro_number
|
ro_number
|
||||||
@@ -2564,6 +2565,8 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
|||||||
}
|
}
|
||||||
percentage
|
percentage
|
||||||
labor_rates
|
labor_rates
|
||||||
|
payout_method
|
||||||
|
commission_rates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2574,6 +2577,7 @@ exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
|
|||||||
productivehrs
|
productivehrs
|
||||||
actualhrs
|
actualhrs
|
||||||
ciecacode
|
ciecacode
|
||||||
|
payout_context
|
||||||
}
|
}
|
||||||
lbr_adjustments
|
lbr_adjustments
|
||||||
ro_number
|
ro_number
|
||||||
@@ -3250,3 +3254,46 @@ exports.SET_JOB_DMS_ID = `mutation SetJobDmsId($id: uuid!, $dms_id: String!, $dm
|
|||||||
kmin
|
kmin
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
|
||||||
|
exports.QUERY_JOB_FOR_SIGNATURE = `query QUERY_JOB_FOR_SIGNATURE($jobid: uuid!) {
|
||||||
|
jobs_by_pk(id: $jobid) {
|
||||||
|
id
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ea
|
||||||
|
ownr_ph1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
|
||||||
|
insert_audit_trail_one(object: $obj) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
exports.QUERY_META_FOR_ESIG_COMPLETION = `query QUERY_META_FOR_ESIG_COMPLETION($jobid: uuid!) {
|
||||||
|
jobs_by_pk(id: $jobid) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
bodyshop {
|
||||||
|
id
|
||||||
|
uselocalmediaserver
|
||||||
|
localmediatoken
|
||||||
|
localmediaserverhttp
|
||||||
|
localmediaservernetwork
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($docInput: documents_insert_input!) {
|
||||||
|
insert_documents_one(object: $docInput) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const sendPaymentNotificationEmail = require("./sendPaymentNotificationEmail");
|
const sendPaymentNotificationEmail = require("./sendPaymentNotificationEmail");
|
||||||
const { INSERT_NEW_PAYMENT, GET_BODYSHOP_BY_ID, GET_JOBS_BY_PKS } = require("../../graphql-client/queries");
|
const { INSERT_NEW_PAYMENT, GET_BODYSHOP_BY_ID, GET_JOBS_BY_PKS } = require("../../graphql-client/queries");
|
||||||
const getPaymentType = require("./getPaymentType");
|
const getPaymentType = require("./getPaymentType");
|
||||||
const moment = require("moment");
|
const moment = require("moment-timezone");
|
||||||
|
|
||||||
const gqlClient = require("../../graphql-client/graphql-client").client;
|
const gqlClient = require("../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const {
|
|||||||
|
|
||||||
const { sendTaskEmail } = require("../../email/sendemail");
|
const { sendTaskEmail } = require("../../email/sendemail");
|
||||||
const getPaymentType = require("./getPaymentType");
|
const getPaymentType = require("./getPaymentType");
|
||||||
const moment = require("moment");
|
const moment = require("moment-timezone");
|
||||||
|
|
||||||
const gqlClient = require("../../graphql-client/graphql-client").client;
|
const gqlClient = require("../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
const { mockSend } = vi.hoisted(() => ({
|
||||||
|
mockSend: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@aws-sdk/client-secrets-manager", () => {
|
||||||
|
return {
|
||||||
|
SecretsManagerClient: vi.fn(() => ({
|
||||||
|
send: mockSend
|
||||||
|
})),
|
||||||
|
GetSecretValueCommand: vi.fn((input) => input)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const getPaymentType = require("../getPaymentType");
|
const getPaymentType = require("../getPaymentType");
|
||||||
const decodeComment = require("../decodeComment");
|
const decodeComment = require("../decodeComment");
|
||||||
const getCptellerUrl = require("../getCptellerUrl");
|
const getCptellerUrl = require("../getCptellerUrl");
|
||||||
@@ -145,28 +158,15 @@ describe("Payment Processing Functions", () => {
|
|||||||
// GetShopCredentials Tests
|
// GetShopCredentials Tests
|
||||||
describe("getShopCredentials", () => {
|
describe("getShopCredentials", () => {
|
||||||
const originalEnv = { ...process.env };
|
const originalEnv = { ...process.env };
|
||||||
let mockSend;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockSend = vi.fn();
|
mockSend.mockReset();
|
||||||
vi.mock("@aws-sdk/client-secrets-manager", () => {
|
|
||||||
return {
|
|
||||||
SecretsManagerClient: vi.fn(() => ({
|
|
||||||
send: mockSend
|
|
||||||
})),
|
|
||||||
GetSecretValueCommand: vi.fn((input) => input)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
process.env.INTELLIPAY_MERCHANTKEY = "test-merchant-key";
|
process.env.INTELLIPAY_MERCHANTKEY = "test-merchant-key";
|
||||||
process.env.INTELLIPAY_APIKEY = "test-api-key";
|
process.env.INTELLIPAY_APIKEY = "test-api-key";
|
||||||
vi.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env = { ...originalEnv };
|
process.env = { ...originalEnv };
|
||||||
vi.restoreAllMocks();
|
|
||||||
vi.unmock("@aws-sdk/client-secrets-manager");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns environment variables in non-production environment", async () => {
|
it("returns environment variables in non-production environment", async () => {
|
||||||
|
|||||||
@@ -315,7 +315,12 @@ function CalculateRatesTotals(ratesList) {
|
|||||||
if (item.mod_lbr_ty) {
|
if (item.mod_lbr_ty) {
|
||||||
//Check to see if it has 0 hours and a price instead.
|
//Check to see if it has 0 hours and a price instead.
|
||||||
//Extend for when there are hours and a price.
|
//Extend for when there are hours and a price.
|
||||||
if (item.lbr_op === "OP14" && item.act_price > 0 && (!item.part_type || item.mod_lb_hrs === 0) && !IsAdditionalCost(item)) {
|
if (
|
||||||
|
item.lbr_op === "OP14" &&
|
||||||
|
item.act_price > 0 &&
|
||||||
|
(!item.part_type || item.mod_lb_hrs === 0) &&
|
||||||
|
!IsAdditionalCost(item)
|
||||||
|
) {
|
||||||
//Scenario where SGI may pay out hours using a part price.
|
//Scenario where SGI may pay out hours using a part price.
|
||||||
if (!ret[item.mod_lbr_ty.toLowerCase()].total) {
|
if (!ret[item.mod_lbr_ty.toLowerCase()].total) {
|
||||||
ret[item.mod_lbr_ty.toLowerCase()].total = Dinero();
|
ret[item.mod_lbr_ty.toLowerCase()].total = Dinero();
|
||||||
@@ -339,38 +344,30 @@ function CalculateRatesTotals(ratesList) {
|
|||||||
let subtotal = Dinero({ amount: 0 });
|
let subtotal = Dinero({ amount: 0 });
|
||||||
let rates_subtotal = Dinero({ amount: 0 });
|
let rates_subtotal = Dinero({ amount: 0 });
|
||||||
|
|
||||||
for (const property in ret) {
|
for (const [property, values] of Object.entries(ret)) {
|
||||||
//Skip calculating mapa and mash if we got the amounts.
|
//Skip calculating mapa and mash if we got the amounts.
|
||||||
if (!((property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine))) {
|
const shouldSkipCalculation = (property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine);
|
||||||
if (!ret[property].total) {
|
|
||||||
ret[property].total = Dinero();
|
if (!shouldSkipCalculation) {
|
||||||
}
|
values.total ??= Dinero();
|
||||||
let threshold;
|
|
||||||
//Check if there is a max for this type.
|
//Check if there is a max for this type and apply it.
|
||||||
if (ratesList.materials && ratesList.materials[property]) {
|
const maxDollar =
|
||||||
//
|
ratesList.materials?.[property]?.cal_maxdlr || ratesList.materials?.[property.toUpperCase()]?.cal_maxdlr;
|
||||||
if (ratesList.materials[property].cal_maxdlr && ratesList.materials[property].cal_maxdlr > 0) {
|
const threshold = maxDollar > 0 ? Dinero({ amount: Math.round(maxDollar * 100) }) : null;
|
||||||
//It has an upper threshhold.
|
|
||||||
threshold = Dinero({
|
|
||||||
amount: Math.round(ratesList.materials[property].cal_maxdlr * 100)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = Dinero({
|
const total = Dinero({
|
||||||
amount: Math.round((ret[property].rate || 0) * 100)
|
amount: Math.round((values.rate || 0) * 100)
|
||||||
}).multiply(ret[property].hours);
|
}).multiply(values.hours);
|
||||||
|
|
||||||
if (threshold && total.greaterThanOrEqual(threshold)) {
|
values.total = values.total.add(threshold && total.greaterThanOrEqual(threshold) ? threshold : total);
|
||||||
ret[property].total = ret[property].total.add(threshold);
|
|
||||||
} else {
|
|
||||||
ret[property].total = ret[property].total.add(total);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subtotal = subtotal.add(ret[property].total);
|
subtotal = subtotal.add(values.total);
|
||||||
|
|
||||||
if (property !== "mapa" && property !== "mash") rates_subtotal = rates_subtotal.add(ret[property].total);
|
if (property !== "mapa" && property !== "mash") {
|
||||||
|
rates_subtotal = rates_subtotal.add(values.total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.subtotal = subtotal;
|
ret.subtotal = subtotal;
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ describe("TotalsServerSide fixture tests", () => {
|
|||||||
|
|
||||||
const fixtureFiles = fs.readdirSync(fixturesDir).filter((f) => f.endsWith(".json"));
|
const fixtureFiles = fs.readdirSync(fixturesDir).filter((f) => f.endsWith(".json"));
|
||||||
|
|
||||||
|
if (fixtureFiles.length === 0) {
|
||||||
|
it.skip("skips when no job total fixtures are present", () => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dummyClient = {
|
const dummyClient = {
|
||||||
request: async () => {
|
request: async () => {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -94,6 +94,47 @@ const generateSignedUploadUrls = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file buffer directly to S3.
|
||||||
|
* Accepts either `req.file.buffer` (e.g. from multer) or `req.body.buffer` (base64 string).
|
||||||
|
*/
|
||||||
|
const uploadFileBuffer = async ({ key, contentType, buffer }) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
throw new Error("key is required");
|
||||||
|
}
|
||||||
|
if (!buffer) {
|
||||||
|
throw new Error("buffer is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPdf = key.toLowerCase().endsWith(".pdf");
|
||||||
|
const client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|
||||||
|
const putParams = {
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
StorageClass: "INTELLIGENT_TIERING"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
putParams.ContentType = contentType;
|
||||||
|
} else if (isPdf) {
|
||||||
|
putParams.ContentType = "application/pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.send(new PutObjectCommand(putParams));
|
||||||
|
|
||||||
|
|
||||||
|
return ({ success: true, key, bucket: imgproxyDestinationBucket });
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
return { success: false, message: error.message, stack: error.stack };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Thumbnail URLS
|
* Get Thumbnail URLS
|
||||||
* @param req
|
* @param req
|
||||||
@@ -500,6 +541,7 @@ const keyStandardize = (doc) => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateSignedUploadUrls,
|
generateSignedUploadUrls,
|
||||||
|
uploadFileBuffer,
|
||||||
getThumbnailUrls,
|
getThumbnailUrls,
|
||||||
getOriginalImageByDocumentId,
|
getOriginalImageByDocumentId,
|
||||||
downloadFiles,
|
downloadFiles,
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
const Dinero = require("dinero.js");
|
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { CalculateExpectedHoursForJob, CalculateTicketsHoursForJob } = require("./pay-all");
|
const { CalculateExpectedHoursForJob, CalculateTicketsHoursForJob } = require("./pay-all");
|
||||||
|
|
||||||
// Dinero.defaultCurrency = "USD";
|
|
||||||
// Dinero.globalLocale = "en-CA";
|
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
|
||||||
|
|
||||||
const get = (obj, key) => {
|
|
||||||
return key.split(".").reduce((o, x) => {
|
|
||||||
return typeof o == "undefined" || o === null ? o : o[x];
|
|
||||||
}, obj);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.calculatelabor = async function (req, res) {
|
exports.calculatelabor = async function (req, res) {
|
||||||
const { jobid, calculateOnly } = req.body;
|
const { jobid } = req.body;
|
||||||
logger.log("job-payroll-calculate-labor", "DEBUG", req.user.email, jobid, null);
|
logger.log("job-payroll-calculate-labor", "DEBUG", req.user.email, jobid, null);
|
||||||
|
|
||||||
const BearerToken = req.BearerToken;
|
const BearerToken = req.BearerToken;
|
||||||
@@ -41,23 +30,19 @@ exports.calculatelabor = async function (req, res) {
|
|||||||
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
||||||
//At the employee level.
|
//At the employee level.
|
||||||
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
//At the labor level
|
const expected = employeeHash[employeeIdKey][laborTypeKey];
|
||||||
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach((rateKey) => {
|
const claimed = ticketHash?.[employeeIdKey]?.[laborTypeKey];
|
||||||
//At the rate level.
|
|
||||||
const expectedHours = employeeHash[employeeIdKey][laborTypeKey][rateKey];
|
|
||||||
//Will the following line fail? Probably if it doesn't exist.
|
|
||||||
const claimedHours = get(ticketHash, `${employeeIdKey}.${laborTypeKey}.${rateKey}`);
|
|
||||||
if (claimedHours) {
|
|
||||||
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
totals.push({
|
if (claimed) {
|
||||||
employeeid: employeeIdKey,
|
delete ticketHash[employeeIdKey][laborTypeKey];
|
||||||
rate: rateKey,
|
}
|
||||||
mod_lbr_ty: laborTypeKey,
|
|
||||||
expectedHours,
|
totals.push({
|
||||||
claimedHours: claimedHours || 0
|
employeeid: employeeIdKey,
|
||||||
});
|
rate: expected.rate,
|
||||||
|
mod_lbr_ty: laborTypeKey,
|
||||||
|
expectedHours: expected.hours,
|
||||||
|
claimedHours: claimed?.hours || 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -65,23 +50,14 @@ exports.calculatelabor = async function (req, res) {
|
|||||||
Object.keys(ticketHash).forEach((employeeIdKey) => {
|
Object.keys(ticketHash).forEach((employeeIdKey) => {
|
||||||
//At the employee level.
|
//At the employee level.
|
||||||
Object.keys(ticketHash[employeeIdKey]).forEach((laborTypeKey) => {
|
Object.keys(ticketHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
//At the labor level
|
const claimed = ticketHash[employeeIdKey][laborTypeKey];
|
||||||
Object.keys(ticketHash[employeeIdKey][laborTypeKey]).forEach((rateKey) => {
|
|
||||||
//At the rate level.
|
|
||||||
const expectedHours = 0;
|
|
||||||
//Will the following line fail? Probably if it doesn't exist.
|
|
||||||
const claimedHours = get(ticketHash, `${employeeIdKey}.${laborTypeKey}.${rateKey}`);
|
|
||||||
if (claimedHours) {
|
|
||||||
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
totals.push({
|
totals.push({
|
||||||
employeeid: employeeIdKey,
|
employeeid: employeeIdKey,
|
||||||
rate: rateKey,
|
rate: claimed.rate,
|
||||||
mod_lbr_ty: laborTypeKey,
|
mod_lbr_ty: laborTypeKey,
|
||||||
expectedHours,
|
expectedHours: 0,
|
||||||
claimedHours: claimedHours || 0
|
claimedHours: claimed.hours || 0
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -101,6 +77,6 @@ exports.calculatelabor = async function (req, res) {
|
|||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(400).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
const Dinero = require("dinero.js");
|
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { CalculateExpectedHoursForJob } = require("./pay-all");
|
const { CalculateExpectedHoursForJob, RoundPayrollHours } = require("./pay-all");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
// Dinero.defaultCurrency = "USD";
|
|
||||||
// Dinero.globalLocale = "en-CA";
|
const normalizePercent = (value) => Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000;
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
|
||||||
|
const getTaskPresetAllocationError = (taskPresets = []) => {
|
||||||
|
const totalsByLaborType = {};
|
||||||
|
|
||||||
|
taskPresets.forEach((taskPreset) => {
|
||||||
|
const percent = normalizePercent(taskPreset?.percent);
|
||||||
|
|
||||||
|
if (!percent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const laborTypes = Array.isArray(taskPreset?.hourstype) ? taskPreset.hourstype : [];
|
||||||
|
|
||||||
|
laborTypes.forEach((laborType) => {
|
||||||
|
if (!laborType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalsByLaborType[laborType] = normalizePercent((totalsByLaborType[laborType] || 0) + percent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const overAllocatedType = Object.entries(totalsByLaborType).find(([, total]) => total > 100);
|
||||||
|
|
||||||
|
if (!overAllocatedType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [laborType, total] = overAllocatedType;
|
||||||
|
return `Task preset percentages for labor type ${laborType} total ${total}% and cannot exceed 100%.`;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.GetTaskPresetAllocationError = getTaskPresetAllocationError;
|
||||||
|
|
||||||
exports.claimtask = async function (req, res) {
|
exports.claimtask = async function (req, res) {
|
||||||
const { jobid, task, calculateOnly, employee } = req.body;
|
const { jobid, task, calculateOnly, employee } = req.body;
|
||||||
@@ -21,12 +52,25 @@ exports.claimtask = async function (req, res) {
|
|||||||
id: jobid
|
id: jobid
|
||||||
});
|
});
|
||||||
|
|
||||||
const theTaskPreset = job.bodyshop.md_tasks_presets.presets.find((tp) => tp.name === task);
|
const taskPresets = job.bodyshop?.md_tasks_presets?.presets || [];
|
||||||
|
const taskPresetAllocationError = getTaskPresetAllocationError(taskPresets);
|
||||||
|
if (taskPresetAllocationError) {
|
||||||
|
res.status(400).json({ success: false, error: taskPresetAllocationError });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theTaskPreset = taskPresets.find((tp) => tp.name === task);
|
||||||
if (!theTaskPreset) {
|
if (!theTaskPreset) {
|
||||||
res.status(400).json({ success: false, error: "Provided task preset not found." });
|
res.status(400).json({ success: false, error: "Provided task preset not found." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const taskAlreadyCompleted = (job.completed_tasks || []).some((completedTask) => completedTask?.name === task);
|
||||||
|
if (taskAlreadyCompleted) {
|
||||||
|
res.status(400).json({ success: false, error: "Provided task preset has already been completed for this job." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//Get all of the assignments that are filtered.
|
//Get all of the assignments that are filtered.
|
||||||
const { assignmentHash, employeeHash } = CalculateExpectedHoursForJob(job, theTaskPreset.hourstype);
|
const { assignmentHash, employeeHash } = CalculateExpectedHoursForJob(job, theTaskPreset.hourstype);
|
||||||
const ticketsToInsert = [];
|
const ticketsToInsert = [];
|
||||||
@@ -35,32 +79,37 @@ exports.claimtask = async function (req, res) {
|
|||||||
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
Object.keys(employeeHash).forEach((employeeIdKey) => {
|
||||||
//At the employee level.
|
//At the employee level.
|
||||||
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
|
||||||
//At the labor level
|
const expected = employeeHash[employeeIdKey][laborTypeKey];
|
||||||
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach((rateKey) => {
|
const expectedHours = RoundPayrollHours(expected.hours * (theTaskPreset.percent / 100));
|
||||||
//At the rate level.
|
|
||||||
const expectedHours = employeeHash[employeeIdKey][laborTypeKey][rateKey] * (theTaskPreset.percent / 100);
|
|
||||||
|
|
||||||
ticketsToInsert.push({
|
ticketsToInsert.push({
|
||||||
task_name: task,
|
task_name: task,
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
bodyshopid: job.bodyshop.id,
|
bodyshopid: job.bodyshop.id,
|
||||||
employeeid: employeeIdKey,
|
employeeid: employeeIdKey,
|
||||||
productivehrs: expectedHours,
|
productivehrs: expectedHours,
|
||||||
rate: rateKey,
|
rate: expected.rate,
|
||||||
ciecacode: laborTypeKey,
|
ciecacode: laborTypeKey,
|
||||||
flat_rate: true,
|
flat_rate: true,
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[laborTypeKey],
|
created_by: employee?.name || req.user.email,
|
||||||
memo: `*Flagged Task* ${theTaskPreset.memo}`
|
payout_context: {
|
||||||
});
|
...(expected.payoutContext || {}),
|
||||||
|
generated_by: req.user.email,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
generated_from: "claimtask",
|
||||||
|
task_name: task
|
||||||
|
},
|
||||||
|
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[laborTypeKey],
|
||||||
|
memo: `*Flagged Task* ${theTaskPreset.memo}`
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (!calculateOnly) {
|
if (!calculateOnly) {
|
||||||
//Insert the time ticekts if we're not just calculating them.
|
//Insert the time ticekts if we're not just calculating them.
|
||||||
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
|
await client.request(queries.INSERT_TIME_TICKETS, {
|
||||||
timetickets: ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0)
|
timetickets: ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0)
|
||||||
});
|
});
|
||||||
const updateResult = await client.request(queries.UPDATE_JOB, {
|
await client.request(queries.UPDATE_JOB, {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: {
|
job: {
|
||||||
status: theTaskPreset.nextstatus,
|
status: theTaskPreset.nextstatus,
|
||||||
@@ -82,6 +131,6 @@ exports.claimtask = async function (req, res) {
|
|||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(400).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,196 @@
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const rdiff = require("recursive-diff");
|
|
||||||
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
// Dinero.defaultCurrency = "USD";
|
|
||||||
// Dinero.globalLocale = "en-CA";
|
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
Dinero.globalFormatRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
|
const PAYOUT_METHODS = {
|
||||||
|
hourly: "hourly",
|
||||||
|
commission: "commission"
|
||||||
|
};
|
||||||
|
|
||||||
|
const CURRENCY_PRECISION = 2;
|
||||||
|
const HOURS_PRECISION = 5;
|
||||||
|
|
||||||
|
const toNumber = (value) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeNumericString = (value) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
const asString = value.toString();
|
||||||
|
|
||||||
|
if (!asString.toLowerCase().includes("e")) {
|
||||||
|
return asString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toFixed(12).replace(/0+$/, "").replace(/\.$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value ?? ""}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const decimalToDinero = (value, errorMessage = "Invalid numeric value.") => {
|
||||||
|
const normalizedValue = normalizeNumericString(value);
|
||||||
|
const parsedValue = Number(normalizedValue);
|
||||||
|
|
||||||
|
if (!Number.isFinite(parsedValue)) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNegative = normalizedValue.startsWith("-");
|
||||||
|
const unsignedValue = normalizedValue.replace(/^[+-]/, "");
|
||||||
|
const [wholePart = "0", fractionPartRaw = ""] = unsignedValue.split(".");
|
||||||
|
const wholeDigits = wholePart.replace(/\D/g, "") || "0";
|
||||||
|
const fractionDigits = fractionPartRaw.replace(/\D/g, "");
|
||||||
|
const amount = Number(`${wholeDigits}${fractionDigits}` || "0") * (isNegative ? -1 : 1);
|
||||||
|
|
||||||
|
return Dinero({
|
||||||
|
amount,
|
||||||
|
precision: fractionDigits.length
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundValueWithDinero = (value, precision, errorMessage) =>
|
||||||
|
decimalToDinero(value, errorMessage).convertPrecision(precision, Dinero.globalRoundingMode).toUnit();
|
||||||
|
|
||||||
|
const roundCurrency = (value, errorMessage = "Invalid currency value.") =>
|
||||||
|
roundValueWithDinero(value, CURRENCY_PRECISION, errorMessage);
|
||||||
|
|
||||||
|
const roundHours = (value, errorMessage = "Invalid hours value.") => roundValueWithDinero(value, HOURS_PRECISION, errorMessage);
|
||||||
|
|
||||||
|
const normalizePayoutMethod = (value) =>
|
||||||
|
value === PAYOUT_METHODS.commission ? PAYOUT_METHODS.commission : PAYOUT_METHODS.hourly;
|
||||||
|
|
||||||
|
const hasOwnValue = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
|
||||||
|
|
||||||
|
const getJobSaleRateField = (laborType) => `rate_${String(laborType || "").toLowerCase()}`;
|
||||||
|
|
||||||
|
const getTeamMemberLabel = (teamMember) => {
|
||||||
|
const fullName = `${teamMember?.employee?.first_name || ""} ${teamMember?.employee?.last_name || ""}`.trim();
|
||||||
|
return fullName || teamMember?.employee?.id || teamMember?.employeeid || "unknown employee";
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRequiredNumber = (value, errorMessage) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFallbackPayoutContext = ({ laborType, rate }) => ({
|
||||||
|
payout_type: "legacy",
|
||||||
|
payout_method: "legacy",
|
||||||
|
cut_percent_applied: null,
|
||||||
|
source_labor_rate: null,
|
||||||
|
source_labor_type: laborType,
|
||||||
|
effective_rate: roundCurrency(rate)
|
||||||
|
});
|
||||||
|
|
||||||
|
function BuildPayoutDetails(job, teamMember, laborType) {
|
||||||
|
const payoutMethod = normalizePayoutMethod(teamMember?.payout_method);
|
||||||
|
const teamMemberLabel = getTeamMemberLabel(teamMember);
|
||||||
|
const sourceLaborRateField = getJobSaleRateField(laborType);
|
||||||
|
|
||||||
|
if (payoutMethod === PAYOUT_METHODS.hourly && !hasOwnValue(teamMember?.labor_rates, laborType)) {
|
||||||
|
throw new Error(`Missing hourly payout rate for ${teamMemberLabel} on labor type ${laborType}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payoutMethod === PAYOUT_METHODS.commission && !hasOwnValue(teamMember?.commission_rates, laborType)) {
|
||||||
|
throw new Error(`Missing commission percent for ${teamMemberLabel} on labor type ${laborType}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payoutMethod === PAYOUT_METHODS.commission && !hasOwnValue(job, sourceLaborRateField)) {
|
||||||
|
throw new Error(`Missing sale rate ${sourceLaborRateField} for labor type ${laborType}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hourlyRate =
|
||||||
|
payoutMethod === PAYOUT_METHODS.hourly
|
||||||
|
? roundCurrency(
|
||||||
|
parseRequiredNumber(
|
||||||
|
teamMember?.labor_rates?.[laborType],
|
||||||
|
`Invalid hourly payout rate for ${teamMemberLabel} on labor type ${laborType}.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const commissionPercent =
|
||||||
|
payoutMethod === PAYOUT_METHODS.commission
|
||||||
|
? roundCurrency(
|
||||||
|
parseRequiredNumber(
|
||||||
|
teamMember?.commission_rates?.[laborType],
|
||||||
|
`Invalid commission percent for ${teamMemberLabel} on labor type ${laborType}.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (commissionPercent !== null && (commissionPercent < 0 || commissionPercent > 100)) {
|
||||||
|
throw new Error(`Commission percent for ${teamMemberLabel} on labor type ${laborType} must be between 0 and 100.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLaborRate =
|
||||||
|
payoutMethod === PAYOUT_METHODS.commission
|
||||||
|
? roundCurrency(
|
||||||
|
parseRequiredNumber(job?.[sourceLaborRateField], `Invalid sale rate ${sourceLaborRateField} for labor type ${laborType}.`)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const effectiveRate =
|
||||||
|
payoutMethod === PAYOUT_METHODS.commission
|
||||||
|
? roundCurrency((sourceLaborRate * toNumber(commissionPercent)) / 100)
|
||||||
|
: hourlyRate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
effectiveRate,
|
||||||
|
payoutContext: {
|
||||||
|
payout_type: payoutMethod === PAYOUT_METHODS.commission ? "cut" : "hourly",
|
||||||
|
payout_method: payoutMethod,
|
||||||
|
cut_percent_applied: commissionPercent,
|
||||||
|
source_labor_rate: sourceLaborRate,
|
||||||
|
source_labor_type: laborType,
|
||||||
|
effective_rate: effectiveRate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function BuildGeneratedPayoutContext({ baseContext, generatedBy, generatedFrom, taskName, usedTicketFallback }) {
|
||||||
|
return {
|
||||||
|
...(baseContext || {}),
|
||||||
|
generated_by: generatedBy,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
generated_from: generatedFrom,
|
||||||
|
task_name: taskName,
|
||||||
|
used_ticket_fallback: Boolean(usedTicketFallback)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllKeys(...objects) {
|
||||||
|
return [...new Set(objects.flatMap((obj) => (obj ? Object.keys(obj) : [])))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayAllMemo({ deltaHours, hasExpected, hasClaimed, userEmail }) {
|
||||||
|
if (!hasClaimed && deltaHours > 0) {
|
||||||
|
return `Add unflagged hours. (${userEmail})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExpected && deltaHours < 0) {
|
||||||
|
return `Remove flagged hours per assignment. (${userEmail})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Adjust flagged hours per assignment. (${userEmail})`;
|
||||||
|
}
|
||||||
|
|
||||||
exports.payall = async function (req, res) {
|
exports.payall = async function (req, res) {
|
||||||
const { jobid, calculateOnly } = req.body;
|
const { jobid } = req.body;
|
||||||
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
|
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
|
||||||
|
|
||||||
const BearerToken = req.BearerToken;
|
const BearerToken = req.BearerToken;
|
||||||
@@ -22,253 +203,183 @@ exports.payall = async function (req, res) {
|
|||||||
id: jobid
|
id: jobid
|
||||||
});
|
});
|
||||||
|
|
||||||
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
|
|
||||||
|
|
||||||
const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job);
|
const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job);
|
||||||
const ticketHash = CalculateTicketsHoursForJob(job);
|
const ticketHash = CalculateTicketsHoursForJob(job);
|
||||||
|
|
||||||
if (assignmentHash.unassigned > 0) {
|
if (assignmentHash.unassigned > 0) {
|
||||||
res.json({ success: false, error: "Not all hours have been assigned." });
|
res.json({ success: false, error: "Not all hours have been assigned." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Calculate how much time each tech should have by labor type.
|
|
||||||
//Doing this order creates a diff of changes on the ticket hash to make it the same as the employee hash.
|
|
||||||
const recursiveDiff = rdiff.getDiff(ticketHash, employeeHash, true);
|
|
||||||
|
|
||||||
const ticketsToInsert = [];
|
const ticketsToInsert = [];
|
||||||
|
const employeeIds = getAllKeys(employeeHash, ticketHash);
|
||||||
|
|
||||||
recursiveDiff.forEach((diff) => {
|
employeeIds.forEach((employeeId) => {
|
||||||
//Every iteration is what we would need to insert into the time ticket hash
|
const expectedByLabor = employeeHash[employeeId] || {};
|
||||||
//so that it would match the employee hash exactly.
|
const claimedByLabor = ticketHash[employeeId] || {};
|
||||||
const path = diffParser(diff);
|
|
||||||
|
|
||||||
if (diff.op === "add") {
|
getAllKeys(expectedByLabor, claimedByLabor).forEach((laborType) => {
|
||||||
// console.log(Object.keys(diff.val));
|
const expected = expectedByLabor[laborType];
|
||||||
if (typeof diff.val === "object" && Object.keys(diff.val).length > 1) {
|
const claimed = claimedByLabor[laborType];
|
||||||
//Multiple values to add.
|
const deltaHours = roundHours((expected?.hours || 0) - (claimed?.hours || 0));
|
||||||
Object.keys(diff.val).forEach((key) => {
|
|
||||||
// console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]);
|
if (deltaHours === 0) {
|
||||||
// console.log("Rate", Object.keys(diff.val[key])[0]);
|
return;
|
||||||
ticketsToInsert.push({
|
|
||||||
task_name: "Pay All",
|
|
||||||
jobid: job.id,
|
|
||||||
bodyshopid: job.bodyshop.id,
|
|
||||||
employeeid: path.employeeid,
|
|
||||||
productivehrs: diff.val[key][Object.keys(diff.val[key])[0]],
|
|
||||||
rate: Object.keys(diff.val[key])[0],
|
|
||||||
ciecacode: key,
|
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[key],
|
|
||||||
flat_rate: true,
|
|
||||||
memo: `Add unflagged hours. (${req.user.email})`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
//Only the 1 value to add.
|
|
||||||
ticketsToInsert.push({
|
|
||||||
task_name: "Pay All",
|
|
||||||
jobid: job.id,
|
|
||||||
bodyshopid: job.bodyshop.id,
|
|
||||||
employeeid: path.employeeid,
|
|
||||||
productivehrs: path.hours,
|
|
||||||
rate: path.rate,
|
|
||||||
ciecacode: path.mod_lbr_ty,
|
|
||||||
flat_rate: true,
|
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty],
|
|
||||||
memo: `Add unflagged hours. (${req.user.email})`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (diff.op === "update") {
|
|
||||||
//An old ticket amount isn't sufficient
|
const effectiveRate = roundCurrency(expected?.rate ?? claimed?.rate);
|
||||||
//We can't modify the existing ticket, it might already be committed. So let's add a new one instead.
|
const payoutContext = BuildGeneratedPayoutContext({
|
||||||
|
baseContext:
|
||||||
|
expected?.payoutContext ||
|
||||||
|
claimed?.payoutContext ||
|
||||||
|
buildFallbackPayoutContext({ laborType, rate: effectiveRate }),
|
||||||
|
generatedBy: req.user.email,
|
||||||
|
generatedFrom: "payall",
|
||||||
|
taskName: "Pay All",
|
||||||
|
usedTicketFallback: !expected && Boolean(claimed)
|
||||||
|
});
|
||||||
|
|
||||||
ticketsToInsert.push({
|
ticketsToInsert.push({
|
||||||
task_name: "Pay All",
|
task_name: "Pay All",
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
bodyshopid: job.bodyshop.id,
|
bodyshopid: job.bodyshop.id,
|
||||||
employeeid: path.employeeid,
|
employeeid: employeeId,
|
||||||
productivehrs: diff.val - diff.oldVal,
|
productivehrs: deltaHours,
|
||||||
rate: path.rate,
|
rate: effectiveRate,
|
||||||
ciecacode: path.mod_lbr_ty,
|
ciecacode: laborType,
|
||||||
|
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[laborType],
|
||||||
flat_rate: true,
|
flat_rate: true,
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty],
|
created_by: req.user.email,
|
||||||
memo: `Adjust flagged hours per assignment. (${req.user.email})`
|
payout_context: payoutContext,
|
||||||
|
memo: buildPayAllMemo({
|
||||||
|
deltaHours,
|
||||||
|
hasExpected: Boolean(expected),
|
||||||
|
hasClaimed: Boolean(claimed),
|
||||||
|
userEmail: req.user.email
|
||||||
|
})
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
//Has to be a delete
|
|
||||||
if (typeof diff.oldVal === "object" && Object.keys(diff.oldVal).length > 1) {
|
|
||||||
//Multiple oldValues to add.
|
|
||||||
Object.keys(diff.oldVal).forEach((key) => {
|
|
||||||
ticketsToInsert.push({
|
|
||||||
task_name: "Pay All",
|
|
||||||
jobid: job.id,
|
|
||||||
bodyshopid: job.bodyshop.id,
|
|
||||||
employeeid: path.employeeid,
|
|
||||||
productivehrs: diff.oldVal[key][Object.keys(diff.oldVal[key])[0]] * -1,
|
|
||||||
rate: Object.keys(diff.oldVal[key])[0],
|
|
||||||
ciecacode: key,
|
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[key],
|
|
||||||
flat_rate: true,
|
|
||||||
memo: `Remove flagged hours per assignment. (${req.user.email})`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
//Only the 1 value to add.
|
|
||||||
ticketsToInsert.push({
|
|
||||||
task_name: "Pay All",
|
|
||||||
jobid: job.id,
|
|
||||||
bodyshopid: job.bodyshop.id,
|
|
||||||
employeeid: path.employeeid,
|
|
||||||
productivehrs: path.hours * -1,
|
|
||||||
rate: path.rate,
|
|
||||||
ciecacode: path.mod_lbr_ty,
|
|
||||||
cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[path.mod_lbr_ty],
|
|
||||||
flat_rate: true,
|
|
||||||
memo: `Remove flagged hours per assignment. (${req.user.email})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
|
const filteredTickets = ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0);
|
||||||
timetickets: ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0)
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json(ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0));
|
if (filteredTickets.length > 0) {
|
||||||
|
await client.request(queries.INSERT_TIME_TICKETS, {
|
||||||
|
timetickets: filteredTickets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(filteredTickets);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-payroll-labor-totals-error", "ERROR", req.user.email, jobid, {
|
logger.log("job-payroll-labor-totals-error", "ERROR", req.user.email, jobid, {
|
||||||
jobid: jobid,
|
jobid,
|
||||||
error: JSON.stringify(error)
|
error: JSON.stringify(error)
|
||||||
});
|
});
|
||||||
res.status(400).json({ error: error.message });
|
res.status(400).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function diffParser(diff) {
|
|
||||||
const type = typeof diff.oldVal;
|
|
||||||
let mod_lbr_ty, rate, hours;
|
|
||||||
|
|
||||||
if (diff.path.length === 1) {
|
|
||||||
if (diff.op === "add") {
|
|
||||||
mod_lbr_ty = Object.keys(diff.val)[0];
|
|
||||||
rate = Object.keys(diff.val[mod_lbr_ty])[0];
|
|
||||||
// hours = diff.oldVal[mod_lbr_ty][rate];
|
|
||||||
} else {
|
|
||||||
mod_lbr_ty = Object.keys(diff.oldVal)[0];
|
|
||||||
rate = Object.keys(diff.oldVal[mod_lbr_ty])[0];
|
|
||||||
// hours = diff.oldVal[mod_lbr_ty][rate];
|
|
||||||
}
|
|
||||||
} else if (diff.path.length === 2) {
|
|
||||||
mod_lbr_ty = diff.path[1];
|
|
||||||
if (diff.op === "add") {
|
|
||||||
rate = Object.keys(diff.val)[0];
|
|
||||||
} else {
|
|
||||||
rate = Object.keys(diff.oldVal)[0];
|
|
||||||
}
|
|
||||||
} else if (diff.path.length === 3) {
|
|
||||||
mod_lbr_ty = diff.path[1];
|
|
||||||
rate = diff.path[2];
|
|
||||||
//hours = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Set the hours
|
|
||||||
if (typeof diff.val === "number" && diff.val !== null && diff.val !== undefined) {
|
|
||||||
hours = diff.val;
|
|
||||||
} else if (diff.val !== null && diff.val !== undefined) {
|
|
||||||
if (diff.path.length === 1) {
|
|
||||||
hours = diff.val[Object.keys(diff.val)[0]][Object.keys(diff.val[Object.keys(diff.val)[0]])];
|
|
||||||
} else {
|
|
||||||
hours = diff.val[Object.keys(diff.val)[0]];
|
|
||||||
}
|
|
||||||
} else if (typeof diff.oldVal === "number" && diff.oldVal !== null && diff.oldVal !== undefined) {
|
|
||||||
hours = diff.oldVal;
|
|
||||||
} else {
|
|
||||||
hours = diff.oldVal[Object.keys(diff.oldVal)[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = {
|
|
||||||
multiVal: false,
|
|
||||||
employeeid: diff.path[0], // Always True
|
|
||||||
mod_lbr_ty,
|
|
||||||
rate,
|
|
||||||
hours
|
|
||||||
};
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
|
function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
|
||||||
const assignmentHash = { unassigned: 0 };
|
const assignmentHash = { unassigned: 0 };
|
||||||
const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid.
|
const employeeHash = {}; // employeeid => Cieca labor type => { hours, rate, payoutContext }
|
||||||
|
const laborTypeFilter = Array.isArray(filterToLbrTypes) ? filterToLbrTypes : null;
|
||||||
|
|
||||||
job.joblines
|
job.joblines
|
||||||
.filter((jobline) => {
|
.filter((jobline) => {
|
||||||
if (!filterToLbrTypes) return true;
|
if (!laborTypeFilter) {
|
||||||
else {
|
return true;
|
||||||
return (
|
|
||||||
filterToLbrTypes.includes(jobline.mod_lbr_ty) ||
|
|
||||||
(jobline.convertedtolbr && filterToLbrTypes.includes(jobline.convertedtolbr_data.mod_lbr_ty))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convertedLaborType = jobline.convertedtolbr ? jobline.convertedtolbr_data?.mod_lbr_ty : null;
|
||||||
|
return laborTypeFilter.includes(jobline.mod_lbr_ty) || (convertedLaborType && laborTypeFilter.includes(convertedLaborType));
|
||||||
})
|
})
|
||||||
.forEach((jobline) => {
|
.forEach((jobline) => {
|
||||||
if (jobline.convertedtolbr) {
|
const laborType = jobline.convertedtolbr ? jobline.convertedtolbr_data?.mod_lbr_ty || jobline.mod_lbr_ty : jobline.mod_lbr_ty;
|
||||||
// Line has been converte to labor. Temporarily re-assign the hours.
|
const laborHours = roundHours(
|
||||||
jobline.mod_lbr_ty = jobline.convertedtolbr_data.mod_lbr_ty;
|
toNumber(jobline.mod_lb_hrs) + (jobline.convertedtolbr ? toNumber(jobline.convertedtolbr_data?.mod_lb_hrs) : 0)
|
||||||
jobline.mod_lb_hrs += jobline.convertedtolbr_data.mod_lb_hrs;
|
);
|
||||||
|
|
||||||
|
if (laborHours === 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (jobline.mod_lb_hrs != 0) {
|
|
||||||
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
|
|
||||||
if (jobline.assigned_team === null) {
|
|
||||||
assignmentHash.unassigned = assignmentHash.unassigned + jobline.mod_lb_hrs;
|
|
||||||
} else {
|
|
||||||
//Line is assigned.
|
|
||||||
if (!assignmentHash[jobline.assigned_team]) {
|
|
||||||
assignmentHash[jobline.assigned_team] = 0;
|
|
||||||
}
|
|
||||||
assignmentHash[jobline.assigned_team] = assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
|
|
||||||
|
|
||||||
//Create the assignment breakdown.
|
if (jobline.assigned_team === null) {
|
||||||
const theTeam = job.bodyshop.employee_teams.find((team) => team.id === jobline.assigned_team);
|
assignmentHash.unassigned = roundHours(assignmentHash.unassigned + laborHours);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
theTeam.employee_team_members.forEach((tm) => {
|
const theTeam = job.bodyshop.employee_teams.find((team) => team.id === jobline.assigned_team);
|
||||||
//Figure out how many hours they are owed at this line, and at what rate.
|
|
||||||
|
|
||||||
if (!employeeHash[tm.employee.id]) {
|
if (!theTeam) {
|
||||||
employeeHash[tm.employee.id] = {};
|
assignmentHash.unassigned = roundHours(assignmentHash.unassigned + laborHours);
|
||||||
}
|
return;
|
||||||
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
|
}
|
||||||
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
|
|
||||||
}
|
|
||||||
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]]) {
|
|
||||||
employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100;
|
assignmentHash[jobline.assigned_team] = roundHours((assignmentHash[jobline.assigned_team] || 0) + laborHours);
|
||||||
employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] =
|
|
||||||
employeeHash[tm.employee.id][jobline.mod_lbr_ty][tm.labor_rates[jobline.mod_lbr_ty]] + hoursOwed;
|
theTeam.employee_team_members.forEach((teamMember) => {
|
||||||
});
|
const employeeId = teamMember.employee.id;
|
||||||
|
const { effectiveRate, payoutContext } = BuildPayoutDetails(job, teamMember, laborType);
|
||||||
|
|
||||||
|
if (!employeeHash[employeeId]) {
|
||||||
|
employeeHash[employeeId] = {};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!employeeHash[employeeId][laborType]) {
|
||||||
|
employeeHash[employeeId][laborType] = {
|
||||||
|
hours: 0,
|
||||||
|
rate: effectiveRate,
|
||||||
|
payoutContext
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursOwed = roundHours((toNumber(teamMember.percentage) * laborHours) / 100);
|
||||||
|
employeeHash[employeeId][laborType].hours = roundHours(employeeHash[employeeId][laborType].hours + hoursOwed);
|
||||||
|
employeeHash[employeeId][laborType].rate = effectiveRate;
|
||||||
|
employeeHash[employeeId][laborType].payoutContext = payoutContext;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { assignmentHash, employeeHash };
|
return { assignmentHash, employeeHash };
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalculateTicketsHoursForJob(job) {
|
function CalculateTicketsHoursForJob(job) {
|
||||||
const ticketHash = {}; // employeeid => Cieca labor type => rate => hours.
|
const ticketHash = {}; // employeeid => Cieca labor type => { hours, rate, payoutContext }
|
||||||
//Calculate how much each employee has been paid so far.
|
|
||||||
job.timetickets.forEach((ticket) => {
|
job.timetickets.forEach((ticket) => {
|
||||||
|
if (!ticket?.employeeid || !ticket?.ciecacode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ticketHash[ticket.employeeid]) {
|
if (!ticketHash[ticket.employeeid]) {
|
||||||
ticketHash[ticket.employeeid] = {};
|
ticketHash[ticket.employeeid] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
|
if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
|
||||||
ticketHash[ticket.employeeid][ticket.ciecacode] = {};
|
ticketHash[ticket.employeeid][ticket.ciecacode] = {
|
||||||
|
hours: 0,
|
||||||
|
rate: roundCurrency(ticket.rate),
|
||||||
|
payoutContext: ticket.payout_context || null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate]) {
|
|
||||||
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = 0;
|
ticketHash[ticket.employeeid][ticket.ciecacode].hours = roundHours(
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode].hours + toNumber(ticket.productivehrs)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ticket.rate !== null && ticket.rate !== undefined) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode].rate = roundCurrency(ticket.rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticket.payout_context) {
|
||||||
|
ticketHash[ticket.employeeid][ticket.ciecacode].payoutContext = ticket.payout_context;
|
||||||
}
|
}
|
||||||
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] =
|
|
||||||
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] + ticket.productivehrs;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return ticketHash;
|
return ticketHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.BuildPayoutDetails = BuildPayoutDetails;
|
||||||
exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob;
|
exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob;
|
||||||
exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;
|
exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;
|
||||||
|
exports.RoundPayrollHours = roundHours;
|
||||||
|
|||||||
367
server/payroll/payroll.test.js
Normal file
367
server/payroll/payroll.test.js
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import mockRequire from "mock-require";
|
||||||
|
|
||||||
|
const logMock = vi.fn();
|
||||||
|
|
||||||
|
let payAllModule;
|
||||||
|
let claimTaskModule;
|
||||||
|
|
||||||
|
const buildBaseJob = (overrides = {}) => ({
|
||||||
|
id: "job-1",
|
||||||
|
completed_tasks: [],
|
||||||
|
rate_laa: 100,
|
||||||
|
bodyshop: {
|
||||||
|
id: "shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
defaults: {
|
||||||
|
costs: {
|
||||||
|
LAA: "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
md_tasks_presets: {
|
||||||
|
presets: []
|
||||||
|
},
|
||||||
|
employee_teams: []
|
||||||
|
},
|
||||||
|
joblines: [],
|
||||||
|
timetickets: [],
|
||||||
|
...overrides
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildReqRes = ({ job, body = {}, userEmail = "payroll@example.com" }) => {
|
||||||
|
const client = {
|
||||||
|
setHeaders: vi.fn().mockReturnThis(),
|
||||||
|
request: vi.fn().mockResolvedValueOnce({ jobs_by_pk: job })
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
body: {
|
||||||
|
jobid: job.id,
|
||||||
|
...body
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
email: userEmail
|
||||||
|
},
|
||||||
|
BearerToken: "Bearer test",
|
||||||
|
userGraphQLClient: client
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
json: vi.fn(),
|
||||||
|
status: vi.fn().mockReturnThis()
|
||||||
|
};
|
||||||
|
|
||||||
|
return { client, req, res };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockRequire.stopAll();
|
||||||
|
mockRequire("../utils/logger", { log: logMock });
|
||||||
|
payAllModule = require("./pay-all");
|
||||||
|
claimTaskModule = require("./claim-task");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("payroll payout helpers", () => {
|
||||||
|
it("defaults team members to hourly payout when no payout method is stored", () => {
|
||||||
|
const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
labor_rates: {
|
||||||
|
LAA: 27.5
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
id: "emp-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LAA"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(effectiveRate).toBe(27.5);
|
||||||
|
expect(payoutContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
payout_type: "hourly",
|
||||||
|
payout_method: "hourly",
|
||||||
|
cut_percent_applied: null,
|
||||||
|
source_labor_rate: null,
|
||||||
|
source_labor_type: "LAA",
|
||||||
|
effective_rate: 27.5
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates commission payout rates from the raw job labor sale rate", () => {
|
||||||
|
const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
|
||||||
|
{
|
||||||
|
rate_laa: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
payout_method: "commission",
|
||||||
|
commission_rates: {
|
||||||
|
LAA: 35
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
id: "emp-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LAA"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(effectiveRate).toBe(42);
|
||||||
|
expect(payoutContext).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
payout_type: "cut",
|
||||||
|
payout_method: "commission",
|
||||||
|
cut_percent_applied: 35,
|
||||||
|
source_labor_rate: 120,
|
||||||
|
source_labor_type: "LAA",
|
||||||
|
effective_rate: 42
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Dinero half-even rounding for stored hourly rates", () => {
|
||||||
|
const { effectiveRate, payoutContext } = payAllModule.BuildPayoutDetails(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
labor_rates: {
|
||||||
|
LAA: 10.005
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
id: "emp-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LAA"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(effectiveRate).toBe(10);
|
||||||
|
expect(payoutContext.effective_rate).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws a useful error when commission configuration is incomplete", () => {
|
||||||
|
expect(() =>
|
||||||
|
payAllModule.BuildPayoutDetails(
|
||||||
|
{
|
||||||
|
rate_laa: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
payout_method: "commission",
|
||||||
|
commission_rates: {},
|
||||||
|
employee: {
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LAA"
|
||||||
|
)
|
||||||
|
).toThrow("Missing commission percent for Jane Doe on labor type LAA.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("payroll routes", () => {
|
||||||
|
it("aggregates claimed hours across prior ticket rates and inserts the remaining delta at the current rate", async () => {
|
||||||
|
const job = buildBaseJob({
|
||||||
|
bodyshop: {
|
||||||
|
id: "shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
defaults: {
|
||||||
|
costs: {
|
||||||
|
LAA: "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
md_tasks_presets: {
|
||||||
|
presets: []
|
||||||
|
},
|
||||||
|
employee_teams: [
|
||||||
|
{
|
||||||
|
id: "team-1",
|
||||||
|
employee_team_members: [
|
||||||
|
{
|
||||||
|
percentage: 100,
|
||||||
|
payout_method: "commission",
|
||||||
|
commission_rates: {
|
||||||
|
LAA: 40
|
||||||
|
},
|
||||||
|
labor_rates: {
|
||||||
|
LAA: 30
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
id: "emp-1",
|
||||||
|
first_name: "Jane",
|
||||||
|
last_name: "Doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
joblines: [
|
||||||
|
{
|
||||||
|
mod_lbr_ty: "LAA",
|
||||||
|
mod_lb_hrs: 10,
|
||||||
|
assigned_team: "team-1",
|
||||||
|
convertedtolbr: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timetickets: [
|
||||||
|
{
|
||||||
|
employeeid: "emp-1",
|
||||||
|
ciecacode: "LAA",
|
||||||
|
productivehrs: 2,
|
||||||
|
rate: 30,
|
||||||
|
payout_context: {
|
||||||
|
payout_method: "hourly"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
employeeid: "emp-1",
|
||||||
|
ciecacode: "LAA",
|
||||||
|
productivehrs: 3,
|
||||||
|
rate: 35,
|
||||||
|
payout_context: {
|
||||||
|
payout_method: "commission"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client, req, res } = buildReqRes({ job });
|
||||||
|
client.request.mockResolvedValueOnce({ insert_timetickets: { affected_rows: 1 } });
|
||||||
|
|
||||||
|
await payAllModule.payall(req, res);
|
||||||
|
|
||||||
|
expect(client.request).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
const insertedTickets = client.request.mock.calls[1][1].timetickets;
|
||||||
|
expect(insertedTickets).toHaveLength(1);
|
||||||
|
expect(insertedTickets[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
task_name: "Pay All",
|
||||||
|
employeeid: "emp-1",
|
||||||
|
productivehrs: 5,
|
||||||
|
rate: 40,
|
||||||
|
ciecacode: "LAA",
|
||||||
|
cost_center: "Body",
|
||||||
|
created_by: "payroll@example.com"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(insertedTickets[0].payout_context).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
payout_method: "commission",
|
||||||
|
cut_percent_applied: 40,
|
||||||
|
source_labor_rate: 100,
|
||||||
|
generated_from: "payall",
|
||||||
|
task_name: "Pay All",
|
||||||
|
used_ticket_fallback: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.json).toHaveBeenCalledWith(insertedTickets);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate claim-task submissions for completed presets", async () => {
|
||||||
|
const job = buildBaseJob({
|
||||||
|
completed_tasks: [{ name: "Disassembly" }],
|
||||||
|
bodyshop: {
|
||||||
|
id: "shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
defaults: {
|
||||||
|
costs: {
|
||||||
|
LAA: "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
md_tasks_presets: {
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: "Disassembly",
|
||||||
|
hourstype: ["LAA"],
|
||||||
|
percent: 50,
|
||||||
|
nextstatus: "In Progress",
|
||||||
|
memo: "Flag disassembly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
employee_teams: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client, req, res } = buildReqRes({
|
||||||
|
job,
|
||||||
|
body: {
|
||||||
|
task: "Disassembly",
|
||||||
|
calculateOnly: false,
|
||||||
|
employee: {
|
||||||
|
name: "Jane Doe",
|
||||||
|
employeeid: "emp-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await claimTaskModule.claimtask(req, res);
|
||||||
|
|
||||||
|
expect(client.request).toHaveBeenCalledTimes(1);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
error: "Provided task preset has already been completed for this job."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects claim-task when task presets over-allocate the same labor type", async () => {
|
||||||
|
const job = buildBaseJob({
|
||||||
|
bodyshop: {
|
||||||
|
id: "shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
defaults: {
|
||||||
|
costs: {
|
||||||
|
LAA: "Body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
md_tasks_presets: {
|
||||||
|
presets: [
|
||||||
|
{
|
||||||
|
name: "Body Prep",
|
||||||
|
hourstype: ["LAA"],
|
||||||
|
percent: 60,
|
||||||
|
nextstatus: "Prep",
|
||||||
|
memo: "Prep body work"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Body Prime",
|
||||||
|
hourstype: ["LAA"],
|
||||||
|
percent: 50,
|
||||||
|
nextstatus: "Prime",
|
||||||
|
memo: "Prime body work"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
employee_teams: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client, req, res } = buildReqRes({
|
||||||
|
job,
|
||||||
|
body: {
|
||||||
|
task: "Body Prep",
|
||||||
|
calculateOnly: true,
|
||||||
|
employee: {
|
||||||
|
name: "Jane Doe",
|
||||||
|
employeeid: "emp-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await claimTaskModule.claimtask(req, res);
|
||||||
|
|
||||||
|
expect(client.request).toHaveBeenCalledTimes(1);
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
success: false,
|
||||||
|
error: "Task preset percentages for labor type LAA total 110% and cannot exceed 100%."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
server/routes/esignRoutes.js
Normal file
17
server/routes/esignRoutes.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
|
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||||
|
const { newEsignDocument, distributeDocument, deleteDocument } = require("../esign/esign-new");
|
||||||
|
const { esignWebhook } = require("../esign/webhook");
|
||||||
|
|
||||||
|
//router.use(validateFirebaseIdTokenMiddleware);
|
||||||
|
|
||||||
|
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
|
||||||
|
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
|
||||||
|
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
|
||||||
|
router.post("/webhook", esignWebhook);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -2,6 +2,9 @@ exports.servertime = (req, res) => {
|
|||||||
res.status(200).send(new Date());
|
res.status(200).send(new Date());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.jsrAuthString =() => {
|
||||||
|
return "Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64")
|
||||||
|
}
|
||||||
exports.jsrAuth = async (req, res) => {
|
exports.jsrAuth = async (req, res) => {
|
||||||
res.send("Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64"));
|
res.send(exports.jsrAuthString());
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user