Compare commits
103 Commits
feature/IO
...
master-AIO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4043bd3d33 | ||
|
|
c1c0b35c8f | ||
|
|
4fd2f034a3 | ||
|
|
aa3b303fe9 | ||
|
|
bd25245290 | ||
|
|
468ed23f73 | ||
|
|
6472b053ed | ||
|
|
322ebd3bc7 | ||
|
|
169070594c | ||
|
|
0f800c5a4c | ||
|
|
0974e69a50 | ||
|
|
345a470731 | ||
|
|
ebde2f1581 | ||
|
|
a45808eb94 | ||
|
|
a2389b1f26 | ||
|
|
ab606a4266 | ||
|
|
da317704c4 | ||
|
|
771573409f | ||
|
|
cb9ccb7e77 | ||
|
|
a5d00d562c | ||
|
|
bdeeea0406 | ||
|
|
297d8afa8a | ||
|
|
3a12597c45 | ||
|
|
72c96f14eb | ||
|
|
de9d47272c | ||
|
|
3fd51f0140 | ||
|
|
84ec68f142 | ||
|
|
22af37e8f1 | ||
|
|
86affddc24 | ||
|
|
57fdffff09 | ||
|
|
e74be56681 | ||
|
|
f5d33a2386 | ||
|
|
edc9ba33c5 | ||
|
|
4586f32f38 | ||
|
|
281e50a43e | ||
|
|
7a50f2a2fe | ||
|
|
0c83f796db | ||
|
|
237c575bab | ||
|
|
a54e74a27d | ||
|
|
87797c7743 | ||
|
|
d227cacd68 | ||
|
|
ef4565d738 | ||
|
|
74eeceacca | ||
|
|
6e566e2f8a | ||
|
|
80697a5259 | ||
|
|
fe8d1f7e95 | ||
|
|
32f3143dca | ||
|
|
0ba207a499 | ||
|
|
e0b113e5d0 | ||
|
|
fc199279d1 | ||
|
|
fcba77fe20 | ||
|
|
f294eafde7 | ||
|
|
e0f55b8e7a | ||
|
|
ef6aee0518 | ||
|
|
d5e643b429 | ||
|
|
88ae1fb1cc | ||
|
|
c6af2b34b2 | ||
|
|
d51dcc0ef2 | ||
|
|
e6178a613d | ||
|
|
2a69115903 | ||
|
|
c8262da440 | ||
|
|
1f41a532e2 | ||
|
|
32e67b14b6 | ||
|
|
d901004751 | ||
|
|
661e019a4d | ||
|
|
82021c1edc | ||
|
|
a6156a70c1 | ||
|
|
0014a5335d | ||
|
|
cd054fcf33 | ||
|
|
5ab54433ff | ||
|
|
62c053ed87 | ||
|
|
6242e0f309 | ||
|
|
614420d7d2 | ||
|
|
3113818a91 | ||
|
|
92a3e57205 | ||
|
|
8a043767cd | ||
|
|
6ca0ebff5f | ||
|
|
a96a1139fa | ||
|
|
483da283dc | ||
|
|
9267e584ff | ||
|
|
69861af88c | ||
|
|
d7294ebba6 | ||
|
|
d9270102b1 | ||
|
|
d416780e63 | ||
|
|
b6cbfb8e45 | ||
|
|
9c97b30e8e | ||
|
|
cc48448a07 | ||
|
|
d991e32501 | ||
|
|
969dd8be8d | ||
|
|
794f64dfba | ||
|
|
220b1c7968 | ||
|
|
7dab60e3bc | ||
|
|
d4c7298334 | ||
|
|
e17b57c705 | ||
|
|
4abc1a7d0f | ||
|
|
255d761210 | ||
|
|
2a5e5d2462 | ||
|
|
6ef56f97c0 | ||
|
|
97d8047a3d | ||
|
|
16220d0a27 | ||
|
|
51fba24a3d | ||
|
|
52f43a600c | ||
|
|
e25174ff97 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -149,3 +149,8 @@ docker_data
|
|||||||
/COPILOT.md
|
/COPILOT.md
|
||||||
/.github/copilot-instructions.md
|
/.github/copilot-instructions.md
|
||||||
/GEMINI.md
|
/GEMINI.md
|
||||||
|
/_reference/select-component-test-plan.md
|
||||||
|
|
||||||
|
.terraform
|
||||||
|
|
||||||
|
terraform.tfvars
|
||||||
File diff suppressed because it is too large
Load Diff
11
client/package-lock.json
generated
11
client/package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@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.5.1",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
||||||
"@firebase/analytics": "^0.10.21",
|
"@firebase/analytics": "^0.10.21",
|
||||||
@@ -2593,6 +2594,16 @@
|
|||||||
"react": ">=16.8.0"
|
"react": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@documenso/embed-react": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@documenso/embed-react/-/embed-react-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-PlkZ3vrdZVBTc0J3xfG2wtPVGmxCxWgpQ/SsdR2oBMdTwsR+rDbj9k+CeTv+M9Xi5tKbLr5Y78bS9Sb8K+ltTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dotenvx/dotenvx": {
|
"node_modules/@dotenvx/dotenvx": {
|
||||||
"version": "1.59.1",
|
"version": "1.59.1",
|
||||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@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.5.1",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
||||||
"@firebase/analytics": "^0.10.21",
|
"@firebase/analytics": "^0.10.21",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useSplitClient } from "@splitsoftware/splitio-react";
|
import { useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Result } from "antd";
|
import { Button, Result } from "antd";
|
||||||
import LogRocket from "logrocket";
|
//import LogRocket from "logrocket";
|
||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -101,13 +101,13 @@ export function App({
|
|||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
|
|
||||||
if (client.getTreatment("LogRocket_Tracking") === "on") {
|
if (client.getTreatment("LogRocket_Tracking") === "on") {
|
||||||
console.log("LR Start");
|
// console.log("LR Start");
|
||||||
LogRocket.init(
|
// LogRocket.init(
|
||||||
InstanceRenderMgr({
|
// InstanceRenderMgr({
|
||||||
imex: "gvfvfw/bodyshopapp",
|
// imex: "gvfvfw/bodyshopapp",
|
||||||
rome: "rome-online/rome-online"
|
// rome: "rome-online/rome-online"
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [bodyshop, client, currentUser.authorized]);
|
}, [bodyshop, client, currentUser.authorized]);
|
||||||
|
|||||||
@@ -509,3 +509,10 @@
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.esignature-embed {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
@@ -435,9 +435,9 @@ export function BillEnterModalLinesComponent({
|
|||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
}),
|
}),
|
||||||
formInput: () => (
|
formInput: () => (
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
style={{ minWidth: "3rem" }}
|
style={{ minWidth: "3rem" }}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
options={
|
options={
|
||||||
@@ -461,7 +461,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
name: [field.name, "location"]
|
name: [field.name, "location"]
|
||||||
}),
|
}),
|
||||||
formInput: () => (
|
formInput: () => (
|
||||||
<Select
|
<Select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
|
options={bodyshop.md_parts_locations.map((loc) => ({ value: loc, label: loc }))}
|
||||||
@@ -495,7 +495,9 @@ export function BillEnterModalLinesComponent({
|
|||||||
{Enhanced_Payroll.treatment === "on" ? (
|
{Enhanced_Payroll.treatment === "on" ? (
|
||||||
<Space>
|
<Space>
|
||||||
{t("joblines.fields.assigned_team", { name: employeeTeamName?.name })}
|
{t("joblines.fields.assigned_team", { name: employeeTeamName?.name })}
|
||||||
{`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`}
|
{jobline
|
||||||
|
? `${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`
|
||||||
|
: null}
|
||||||
</Space>
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -506,10 +508,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
|
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select allowClear options={CiecaSelect(false, true)} />
|
||||||
allowClear
|
|
||||||
options={CiecaSelect(false, true)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{Enhanced_Payroll.treatment === "on" ? (
|
{Enhanced_Payroll.treatment === "on" ? (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Col } from "antd";
|
import { Button, Checkbox, Col } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -49,7 +49,13 @@ export default function PBSCustomerSelector({ bodyshop, socket }) {
|
|||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: t("jobs.fields.dms.id"), dataIndex: "ContactId", key: "ContactId" },
|
{ title: t("jobs.fields.dms.id"), dataIndex: "Code", key: "ContactId" },
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.IsARCustomer"),
|
||||||
|
dataIndex: "IsARCustomer",
|
||||||
|
key: "IsARCustomer",
|
||||||
|
render: (text, record) => <Checkbox checked={record.IsARCustomer} disabled />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.dms.name1"),
|
title: t("jobs.fields.dms.name1"),
|
||||||
key: "name1",
|
key: "name1",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Result } from "antd";
|
import { Result, theme } from "antd";
|
||||||
import * as markerjs2 from "markerjs2";
|
import * as markerjs2 from "markerjs2";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -9,6 +9,12 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
|||||||
import { handleUpload } from "../documents-local-upload/documents-local-upload.utility";
|
import { handleUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import {
|
||||||
|
addGreyscaleButtonToMarkerArea,
|
||||||
|
addImageHistoryUndoToMarkerArea,
|
||||||
|
applyGreyscaleToMarkerAreaImage,
|
||||||
|
setMarkerAreaImageSource
|
||||||
|
} from "./document-editor.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -24,7 +30,9 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageLoading, setImageLoading] = useState(true);
|
const [imageLoading, setImageLoading] = useState(true);
|
||||||
const markerArea = useRef(null);
|
const markerArea = useRef(null);
|
||||||
|
const imageHistory = useRef([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { token } = theme.useToken();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
@@ -32,6 +40,7 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
async (dataUrl) => {
|
async (dataUrl) => {
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
setLoading(true);
|
||||||
const blob = await b64toBlob(dataUrl);
|
const blob = await b64toBlob(dataUrl);
|
||||||
const nameWithoutExt = filename.split(".").slice(0, -1).join(".").trim();
|
const nameWithoutExt = filename.split(".").slice(0, -1).join(".").trim();
|
||||||
const parts = nameWithoutExt.split("-");
|
const parts = nameWithoutExt.split("-");
|
||||||
@@ -70,6 +79,23 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
[filename, jobid, notification, uploading]
|
[filename, jobid, notification, uploading]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleGreyscale = useCallback(() => {
|
||||||
|
if (!imgRef.current || loading || uploaded || imageLoading || !imageLoaded) return;
|
||||||
|
|
||||||
|
imageHistory.current.push(imgRef.current.src);
|
||||||
|
applyGreyscaleToMarkerAreaImage(markerArea.current, imgRef.current);
|
||||||
|
}, [imageLoaded, imageLoading, loading, uploaded]);
|
||||||
|
|
||||||
|
const undoImageEdit = useCallback(() => {
|
||||||
|
if (!imgRef.current) return;
|
||||||
|
|
||||||
|
const previousSrc = imageHistory.current.pop();
|
||||||
|
|
||||||
|
if (previousSrc) {
|
||||||
|
setMarkerAreaImageSource(markerArea.current, imgRef.current, previousSrc);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imgRef.current !== null && imageLoaded && !markerArea.current) {
|
if (imgRef.current !== null && imageLoaded && !markerArea.current) {
|
||||||
// create a marker.js MarkerArea
|
// create a marker.js MarkerArea
|
||||||
@@ -93,8 +119,10 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
markerArea.current.renderImageQuality = 1;
|
markerArea.current.renderImageQuality = 1;
|
||||||
//markerArea.current.settings.displayMode = "inline";
|
//markerArea.current.settings.displayMode = "inline";
|
||||||
markerArea.current.show();
|
markerArea.current.show();
|
||||||
|
addGreyscaleButtonToMarkerArea(markerArea.current, handleGreyscale, t("documents.labels.greyscale"));
|
||||||
|
addImageHistoryUndoToMarkerArea(markerArea.current, () => imageHistory.current.length > 0, undoImageEdit);
|
||||||
}
|
}
|
||||||
}, [triggerUpload, imageLoaded]);
|
}, [handleGreyscale, imageLoaded, t, triggerUpload, undoImageEdit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageUrl) return;
|
if (!imageUrl) return;
|
||||||
@@ -106,6 +134,7 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(imageUrl, { responseType: "blob", signal: controller.signal });
|
const response = await axios.get(imageUrl, { responseType: "blob", signal: controller.signal });
|
||||||
const blobUrl = URL.createObjectURL(response.data);
|
const blobUrl = URL.createObjectURL(response.data);
|
||||||
|
imageHistory.current = [];
|
||||||
setLoadedImageUrl((prevUrl) => {
|
setLoadedImageUrl((prevUrl) => {
|
||||||
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
||||||
return blobUrl;
|
return blobUrl;
|
||||||
@@ -142,7 +171,7 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ background: token.colorBgBase, color: token.colorText, minHeight: "100vh" }}>
|
||||||
{!loading && !uploaded && loadedImageUrl && (
|
{!loading && !uploaded && loadedImageUrl && (
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
@@ -158,7 +187,12 @@ export function DocumentEditorLocalComponent({ imageUrl, filename, jobid }) {
|
|||||||
{(loading || imageLoading || !imageLoaded) && !uploaded && (
|
{(loading || imageLoading || !imageLoaded) && !uploaded && (
|
||||||
<LoadingSpinner message={t("documents.labels.uploading")} />
|
<LoadingSpinner message={t("documents.labels.uploading")} />
|
||||||
)}
|
)}
|
||||||
{uploaded && <Result status="success" title={t("documents.successes.edituploaded")} />}
|
{uploaded && (
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title={<span style={{ color: token.colorText }}>{t("documents.successes.edituploaded")}</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
//import "tui-image-editor/dist/tui-image-editor.css";
|
//import "tui-image-editor/dist/tui-image-editor.css";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Result } from "antd";
|
import { Result, theme } from "antd";
|
||||||
import * as markerjs2 from "markerjs2";
|
import * as markerjs2 from "markerjs2";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { handleUpload } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility.js";
|
import { handleUpload } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import {
|
||||||
|
addGreyscaleButtonToMarkerArea,
|
||||||
|
addImageHistoryUndoToMarkerArea,
|
||||||
|
applyGreyscaleToMarkerAreaImage,
|
||||||
|
setMarkerAreaImageSource
|
||||||
|
} from "./document-editor.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -27,7 +33,9 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [imageLoading, setImageLoading] = useState(true);
|
const [imageLoading, setImageLoading] = useState(true);
|
||||||
const markerArea = useRef(null);
|
const markerArea = useRef(null);
|
||||||
|
const imageHistory = useRef([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { token } = theme.useToken();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const triggerUpload = useCallback(
|
const triggerUpload = useCallback(
|
||||||
@@ -57,6 +65,23 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
[bodyshop, currentUser, document, notification]
|
[bodyshop, currentUser, document, notification]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleGreyscale = useCallback(() => {
|
||||||
|
if (!imgRef.current || loading || uploaded || imageLoading || !imageLoaded) return;
|
||||||
|
|
||||||
|
imageHistory.current.push(imgRef.current.src);
|
||||||
|
applyGreyscaleToMarkerAreaImage(markerArea.current, imgRef.current);
|
||||||
|
}, [imageLoaded, imageLoading, loading, uploaded]);
|
||||||
|
|
||||||
|
const undoImageEdit = useCallback(() => {
|
||||||
|
if (!imgRef.current) return;
|
||||||
|
|
||||||
|
const previousSrc = imageHistory.current.pop();
|
||||||
|
|
||||||
|
if (previousSrc) {
|
||||||
|
setMarkerAreaImageSource(markerArea.current, imgRef.current, previousSrc);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imgRef.current !== null && imageLoaded && !markerArea.current) {
|
if (imgRef.current !== null && imageLoaded && !markerArea.current) {
|
||||||
// create a marker.js MarkerArea
|
// create a marker.js MarkerArea
|
||||||
@@ -80,8 +105,10 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
markerArea.current.renderImageQuality = 1;
|
markerArea.current.renderImageQuality = 1;
|
||||||
//markerArea.current.settings.displayMode = "inline";
|
//markerArea.current.settings.displayMode = "inline";
|
||||||
markerArea.current.show();
|
markerArea.current.show();
|
||||||
|
addGreyscaleButtonToMarkerArea(markerArea.current, handleGreyscale, t("documents.labels.greyscale"));
|
||||||
|
addImageHistoryUndoToMarkerArea(markerArea.current, () => imageHistory.current.length > 0, undoImageEdit);
|
||||||
}
|
}
|
||||||
}, [triggerUpload, imageLoaded]);
|
}, [handleGreyscale, imageLoaded, t, triggerUpload, undoImageEdit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!document?.id) return;
|
if (!document?.id) return;
|
||||||
@@ -100,6 +127,7 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const blobUrl = URL.createObjectURL(response.data);
|
const blobUrl = URL.createObjectURL(response.data);
|
||||||
|
imageHistory.current = [];
|
||||||
setImageUrl((prevUrl) => {
|
setImageUrl((prevUrl) => {
|
||||||
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
||||||
return blobUrl;
|
return blobUrl;
|
||||||
@@ -134,7 +162,7 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ background: token.colorBgBase, color: token.colorText, minHeight: "100vh" }}>
|
||||||
{!loading && !uploaded && imageUrl && (
|
{!loading && !uploaded && imageUrl && (
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
@@ -150,7 +178,12 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
|
|||||||
{(loading || imageLoading || !imageLoaded) && !uploaded && (
|
{(loading || imageLoading || !imageLoaded) && !uploaded && (
|
||||||
<LoadingSpinner message={t("documents.labels.uploading")} />
|
<LoadingSpinner message={t("documents.labels.uploading")} />
|
||||||
)}
|
)}
|
||||||
{uploaded && <Result status="success" title={t("documents.successes.edituploaded")} />}
|
{uploaded && (
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title={<span style={{ color: token.colorText }}>{t("documents.successes.edituploaded")}</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
client/src/components/document-editor/document-editor.utility.js
Normal file
123
client/src/components/document-editor/document-editor.utility.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Converts an image element to a greyscale data URL.
|
||||||
|
* @param imageElement
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function convertImageElementToGreyscaleDataUrl(imageElement) {
|
||||||
|
if (!imageElement?.naturalWidth || !imageElement?.naturalHeight) {
|
||||||
|
throw new Error("Image must be loaded before it can be converted to greyscale.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = imageElement.naturalWidth;
|
||||||
|
canvas.height = imageElement.naturalHeight;
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
context.drawImage(imageElement, 0, 0);
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const pixels = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const luminance = Math.round(pixels[i] * 0.299 + pixels[i + 1] * 0.587 + pixels[i + 2] * 0.114);
|
||||||
|
pixels[i] = luminance;
|
||||||
|
pixels[i + 1] = luminance;
|
||||||
|
pixels[i + 2] = luminance;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
return canvas.toDataURL("image/jpeg", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a greyscale button to the marker area controls if it doesn't already exist.
|
||||||
|
* @param markerArea
|
||||||
|
* @param onGreyscale
|
||||||
|
* @param title
|
||||||
|
*/
|
||||||
|
export function addGreyscaleButtonToMarkerArea(markerArea, onGreyscale, title) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const renderButton = markerArea?.coverDiv?.querySelector?.('[data-action="render"]');
|
||||||
|
|
||||||
|
if (!renderButton || markerArea.coverDiv.querySelector('[data-action="greyscale"]')) return;
|
||||||
|
|
||||||
|
const greyscaleButton = document.createElement("div");
|
||||||
|
greyscaleButton.className = renderButton.className;
|
||||||
|
greyscaleButton.innerHTML =
|
||||||
|
'<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20V2zm0 2.25v15.5a7.75 7.75 0 0 1 0-15.5z"/></svg>';
|
||||||
|
greyscaleButton.setAttribute("role", "button");
|
||||||
|
greyscaleButton.setAttribute("data-action", "greyscale");
|
||||||
|
greyscaleButton.setAttribute("aria-label", title);
|
||||||
|
greyscaleButton.title = title;
|
||||||
|
greyscaleButton.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onGreyscale();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderButton.parentElement.insertBefore(greyscaleButton, renderButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a greyscale filter to the image in the marker area and updates the image source.
|
||||||
|
* @param markerArea
|
||||||
|
* @param imageElement
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function applyGreyscaleToMarkerAreaImage(markerArea, imageElement) {
|
||||||
|
const dataUrl = convertImageElementToGreyscaleDataUrl(imageElement);
|
||||||
|
|
||||||
|
setMarkerAreaImageSource(markerArea, imageElement, dataUrl);
|
||||||
|
|
||||||
|
return dataUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the image source for the marker area and updates the editing target if it's an image element.
|
||||||
|
* @param markerArea
|
||||||
|
* @param imageElement
|
||||||
|
* @param src
|
||||||
|
*/
|
||||||
|
export function setMarkerAreaImageSource(markerArea, imageElement, src) {
|
||||||
|
imageElement.src = src;
|
||||||
|
|
||||||
|
if (markerArea?.editingTarget instanceof HTMLImageElement) {
|
||||||
|
markerArea.editingTarget.src = src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds undo functionality for image edits to the marker area by tracking the state before and after undo actions.
|
||||||
|
* @param markerArea
|
||||||
|
* @param canUndoImage
|
||||||
|
* @param undoImage
|
||||||
|
*/
|
||||||
|
export function addImageHistoryUndoToMarkerArea(markerArea, canUndoImage, undoImage) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const undoButton = markerArea?.coverDiv?.querySelector?.('[data-action="undo"]');
|
||||||
|
|
||||||
|
if (!undoButton || undoButton.dataset.imageHistoryUndo === "true") return;
|
||||||
|
|
||||||
|
let markerStateBeforeUndo = null;
|
||||||
|
|
||||||
|
undoButton.dataset.imageHistoryUndo = "true";
|
||||||
|
undoButton.addEventListener(
|
||||||
|
"click",
|
||||||
|
() => {
|
||||||
|
markerStateBeforeUndo = JSON.stringify(markerArea.getState(true));
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
undoButton.addEventListener("click", () => {
|
||||||
|
const markerStateAfterUndo = JSON.stringify(markerArea.getState(true));
|
||||||
|
|
||||||
|
if (markerStateBeforeUndo === markerStateAfterUndo && canUndoImage()) {
|
||||||
|
undoImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
markerStateBeforeUndo = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import i18n from "i18next";
|
|||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||||
|
import { replaceAccents } from "../../utils/replaceAccents.js";
|
||||||
import client from "../../utils/GraphQLClient";
|
import client from "../../utils/GraphQLClient";
|
||||||
|
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
@@ -144,32 +145,3 @@ export const uploadToS3 = async (
|
|||||||
if (onError) onError(JSON.stringify(error.message));
|
if (onError) onError(JSON.stringify(error.message));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Upload } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setEsignatureContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context,
|
||||||
|
modal: "esignature"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const notification = useNotification();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!hasDocumensoApiKey(bodyshop)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadCustomDocument = async ({ file, onError, onSuccess }) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("document", file);
|
||||||
|
formData.append("jobid", jobId);
|
||||||
|
formData.append("bodyshop", JSON.stringify(bodyshop));
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { token, documentId, envelopeId }
|
||||||
|
} = await axios.post("/esign/new-custom", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setEsignatureContext({ context: { token, documentId, envelopeId, jobid: jobId } });
|
||||||
|
onSuccess?.({ token, documentId, envelopeId });
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: t("esignature.errors.upload_title"),
|
||||||
|
description: error?.response?.data?.error || error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
onError?.(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload
|
||||||
|
accept="application/pdf,.pdf"
|
||||||
|
beforeUpload={(file) => {
|
||||||
|
if (file.type === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.error({
|
||||||
|
title: t("esignature.errors.upload_title"),
|
||||||
|
description: t("esignature.errors.pdf_only")
|
||||||
|
});
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}}
|
||||||
|
customRequest={uploadCustomDocument}
|
||||||
|
maxCount={1}
|
||||||
|
showUploadList={false}
|
||||||
|
multiple={false}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />} loading={loading}>
|
||||||
|
{t("esignature.actions.upload_document")}
|
||||||
|
</Button>
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EsignatureCustomDocument);
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
||||||
|
import { Modal, notification, Result } 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, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { useState } from "react";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
esignatureModal: selectEsignature,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("esignature"))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop, currentUser }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { open, context } = esignatureModal;
|
||||||
|
const { token, envelopeId, documentId, jobid } = context;
|
||||||
|
const [distributing, setDistributing] = useState(false);
|
||||||
|
|
||||||
|
if (!hasDocumensoApiKey(bodyshop)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
title={InstanceRenderManager({
|
||||||
|
imex: t("jobs.labels.esignature_imex"),
|
||||||
|
rome: t("jobs.labels.esignature_rome")
|
||||||
|
})}
|
||||||
|
onOk={async () => {
|
||||||
|
try {
|
||||||
|
setDistributing(true);
|
||||||
|
await axios.post("/esign/distribute", {
|
||||||
|
documentId,
|
||||||
|
envelopeId,
|
||||||
|
jobid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleModalVisible();
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.distribute_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDistributing(false);
|
||||||
|
}}
|
||||||
|
onCancel={async () => {
|
||||||
|
try {
|
||||||
|
await axios.post("/esign/delete", {
|
||||||
|
documentId,
|
||||||
|
envelopeId,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleModalVisible();
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.cancel_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
okButtonProps={{ loading: distributing }}
|
||||||
|
okText={t("esignature.actions.distribute")}
|
||||||
|
destroyOnHidden
|
||||||
|
width={"80%"}
|
||||||
|
>
|
||||||
|
<div style={{ height: "80vh", width: "100%" }}>
|
||||||
|
{token ? (
|
||||||
|
<EmbedUpdateDocumentV1
|
||||||
|
presignToken={token}
|
||||||
|
host="https://sign.imex.online"
|
||||||
|
documentId={documentId}
|
||||||
|
externalId={`${jobid}|${currentUser?.email}`}
|
||||||
|
className="esignature-embed"
|
||||||
|
onDocumentUpdated={(data) => {
|
||||||
|
console.log("Document updated:", data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Result status="warning" title={t("esignature.errors.no_token")} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EsignatureModalContainer);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Col, Row, Tag } from "antd";
|
import { Button, Card, Checkbox, Col, Row, Space, Tag } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -12,6 +12,9 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -23,6 +26,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobAuditTrail);
|
|||||||
|
|
||||||
export function JobAuditTrail({ bodyshop, jobId }) {
|
export function JobAuditTrail({ bodyshop, jobId }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const notification = useNotification();
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
|
const { loading, data, refetch } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
variables: { jobid: jobId },
|
variables: { jobid: jobId },
|
||||||
skip: !jobId,
|
skip: !jobId,
|
||||||
@@ -53,6 +58,145 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
const esigColumns = [
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.created_at"),
|
||||||
|
dataIndex: "created_at",
|
||||||
|
key: "created_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.updated_at"),
|
||||||
|
dataIndex: "updated_at",
|
||||||
|
key: "updated_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.title"),
|
||||||
|
dataIndex: "title",
|
||||||
|
key: "title",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.external_document_id"),
|
||||||
|
dataIndex: "external_document_id",
|
||||||
|
key: "external_document_id",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
render: (text) => (
|
||||||
|
<BlurWrapperComponent featureName="audit" bypass>
|
||||||
|
<div>{text}</div>
|
||||||
|
</BlurWrapperComponent>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.opened"),
|
||||||
|
dataIndex: "opened",
|
||||||
|
key: "opened",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.rejected"),
|
||||||
|
dataIndex: "rejected",
|
||||||
|
key: "rejected",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.completed"),
|
||||||
|
dataIndex: "completed",
|
||||||
|
key: "completed",
|
||||||
|
render: (text) => <Checkbox checked={text} disabled />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("esignature.fields.completed_at"),
|
||||||
|
dataIndex: "completed_at",
|
||||||
|
key: "completed_at",
|
||||||
|
render: (text) => <DateTimeFormatter>{text}</DateTimeFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
render: (_text, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
disabled={record.completed_at !== null || record.status === "REJECTED"}
|
||||||
|
onClick={async () => {
|
||||||
|
logImEXEvent("job_esig_delete", {});
|
||||||
|
try {
|
||||||
|
await axios.post("/esign/delete", {
|
||||||
|
documentId: record.external_document_id,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting document:", error?.response?.data || error.message);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.delete_error"),
|
||||||
|
description: error?.response?.data?.error || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("esignature.actions.delete")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
logImEXEvent("job_esig_redistribute", {});
|
||||||
|
try {
|
||||||
|
await axios.post("/esign/redistribute", {
|
||||||
|
documentId: record.external_document_id,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
|
||||||
|
//Pop the success notification. Possible audit requery required.
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error viewing document:", error?.response?.data || error.message);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.view_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("esignature.actions.redistribute")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
logImEXEvent("job_esig_view", {});
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/esign/view", {
|
||||||
|
documentId: record.external_document_id,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
});
|
||||||
|
window.open(response.data?.document?.downloadUrl, "_blank");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error viewing document:", error?.response?.data || error.message);
|
||||||
|
notification.error({
|
||||||
|
message: t("esignature.view_error"),
|
||||||
|
description: error?.response?.data?.message || error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("esignature.actions.view")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
const emailColumns = [
|
const emailColumns = [
|
||||||
{
|
{
|
||||||
title: t("audit.fields.created"),
|
title: t("audit.fields.created"),
|
||||||
@@ -184,6 +328,20 @@ export function JobAuditTrail({ bodyshop, jobId }) {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
{esignatureEnabled && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title={t("jobs.labels.esignatures")}>
|
||||||
|
<ResponsiveTable
|
||||||
|
loading={loading}
|
||||||
|
columns={esigColumns}
|
||||||
|
mobileColumnKeys={["title", "status"]}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: true }}
|
||||||
|
dataSource={data ? data.esignature_documents : []}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-ass
|
|||||||
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
||||||
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
||||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||||
|
import { buildInHouseBillLines } from "./job-lines.in-house-bill-lines.utils";
|
||||||
import JobLinesExpander from "./job-lines-expander.component";
|
import JobLinesExpander from "./job-lines-expander.component";
|
||||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
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";
|
||||||
@@ -595,16 +596,7 @@ export function JobLinesComponent({
|
|||||||
isinhouse: true,
|
isinhouse: true,
|
||||||
date: dayjs(),
|
date: dayjs(),
|
||||||
total: 0,
|
total: 0,
|
||||||
billlines: selectedLines.map((p) => ({
|
billlines: buildInHouseBillLines(selectedLines)
|
||||||
joblineid: p.id,
|
|
||||||
actual_price: p.act_price,
|
|
||||||
actual_cost: 0,
|
|
||||||
line_desc: p.line_desc,
|
|
||||||
line_remarks: p.line_remarks,
|
|
||||||
part_type: p.part_type,
|
|
||||||
quantity: p.quantity || 1,
|
|
||||||
applicable_taxes: { local: false, state: false, federal: false }
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const buildInHouseBillLines = (lines) =>
|
||||||
|
lines.map((line) => ({
|
||||||
|
joblineid: line.id,
|
||||||
|
actual_price: line.act_price,
|
||||||
|
actual_cost: 0,
|
||||||
|
line_desc: line.line_desc,
|
||||||
|
line_remarks: line.line_remarks,
|
||||||
|
part_type: line.part_type,
|
||||||
|
quantity: line.part_qty ?? line.quantity ?? 1,
|
||||||
|
applicable_taxes: { local: false, state: false, federal: false }
|
||||||
|
}));
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildInHouseBillLines } from "./job-lines.in-house-bill-lines.utils";
|
||||||
|
|
||||||
|
describe("buildInHouseBillLines", () => {
|
||||||
|
it("carries job line part quantity into the in-house bill line", () => {
|
||||||
|
const billLines = buildInHouseBillLines([
|
||||||
|
{
|
||||||
|
id: "job-line-1",
|
||||||
|
act_price: 125,
|
||||||
|
line_desc: "Door shell",
|
||||||
|
line_remarks: "Left",
|
||||||
|
part_type: "PAA",
|
||||||
|
part_qty: 3
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(billLines[0]).toMatchObject({
|
||||||
|
joblineid: "job-line-1",
|
||||||
|
actual_price: 125,
|
||||||
|
actual_cost: 0,
|
||||||
|
line_desc: "Door shell",
|
||||||
|
line_remarks: "Left",
|
||||||
|
part_type: "PAA",
|
||||||
|
quantity: 3,
|
||||||
|
applicable_taxes: { local: false, state: false, federal: false }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to legacy quantity and then one when part quantity is absent", () => {
|
||||||
|
expect(buildInHouseBillLines([{ id: "legacy", quantity: 2 }])[0].quantity).toBe(2);
|
||||||
|
expect(buildInHouseBillLines([{ id: "missing" }])[0].quantity).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -67,22 +67,25 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||||
<Select allowClear options={[
|
<Select
|
||||||
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
allowClear
|
||||||
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
options={[
|
||||||
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
{ value: "LAA", label: t("joblines.fields.lbr_types.LAA") },
|
||||||
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
{ value: "LAB", label: t("joblines.fields.lbr_types.LAB") },
|
||||||
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
{ value: "LAD", label: t("joblines.fields.lbr_types.LAD") },
|
||||||
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
{ value: "LAE", label: t("joblines.fields.lbr_types.LAE") },
|
||||||
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
{ value: "LAF", label: t("joblines.fields.lbr_types.LAF") },
|
||||||
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
{ value: "LAG", label: t("joblines.fields.lbr_types.LAG") },
|
||||||
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
{ value: "LAM", label: t("joblines.fields.lbr_types.LAM") },
|
||||||
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
{ value: "LAR", label: t("joblines.fields.lbr_types.LAR") },
|
||||||
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
{ value: "LAS", label: t("joblines.fields.lbr_types.LAS") },
|
||||||
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
{ value: "LAU", label: t("joblines.fields.lbr_types.LAU") },
|
||||||
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
{ value: "LA1", label: t("joblines.fields.lbr_types.LA1") },
|
||||||
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
{ value: "LA2", label: t("joblines.fields.lbr_types.LA2") },
|
||||||
]} />
|
{ value: "LA3", label: t("joblines.fields.lbr_types.LA3") },
|
||||||
|
{ value: "LA4", label: t("joblines.fields.lbr_types.LA4") }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc">
|
<Form.Item label={t("joblines.fields.op_code_desc")} name="op_code_desc">
|
||||||
<Input />
|
<Input />
|
||||||
@@ -128,21 +131,27 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
||||||
<Select allowClear options={[
|
<Select
|
||||||
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
|
allowClear
|
||||||
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
|
options={[
|
||||||
{ value: "PAE", label: t("joblines.fields.part_types.PAE") },
|
{ value: "PAA", label: t("joblines.fields.part_types.PAA") },
|
||||||
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
|
{ value: "PAC", label: t("joblines.fields.part_types.PAC") },
|
||||||
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
|
{ value: "PAE", label: t("joblines.fields.part_types.PAE") },
|
||||||
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
|
{ value: "PAL", label: t("joblines.fields.part_types.PAL") },
|
||||||
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
|
{ value: "PAM", label: t("joblines.fields.part_types.PAM") },
|
||||||
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
|
{ value: "PAN", label: t("joblines.fields.part_types.PAN") },
|
||||||
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
|
{ value: "PAO", label: t("joblines.fields.part_types.PAO") },
|
||||||
]} />
|
{ value: "PAR", label: t("joblines.fields.part_types.PAR") },
|
||||||
|
{ value: "PAS", label: t("joblines.fields.part_types.PAS") }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
|
<Form.Item label={t("joblines.fields.oem_partno")} name="oem_partno">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("joblines.fields.alt_partno")} name="alt_partno">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.part_qty")}
|
label={t("joblines.fields.part_qty")}
|
||||||
name="part_qty"
|
name="part_qty"
|
||||||
|
|||||||
@@ -224,14 +224,10 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item name={["ins_co_nm"]} label={t("jobs.fields.ins_co_nm")} rules={[{ required: true }]}>
|
||||||
name={["ins_co_nm"]}
|
|
||||||
label={t("jobs.fields.ins_co_nm")}
|
|
||||||
rules={[{ required: true }]}
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
showSearch={{
|
showSearch={{
|
||||||
optionFilterProp:'label'
|
optionFilterProp: "label"
|
||||||
}}
|
}}
|
||||||
options={insuranceOptions}
|
options={insuranceOptions}
|
||||||
/>
|
/>
|
||||||
@@ -250,7 +246,7 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
|
|||||||
label={t("jobs.fields.referralsource")}
|
label={t("jobs.fields.referralsource")}
|
||||||
rules={[{ required: bodyshop.enforce_referral }]}
|
rules={[{ required: bodyshop.enforce_referral }]}
|
||||||
>
|
>
|
||||||
<Select options={referralOptions} />
|
<Select showSearch={{ optionFilterProp: "label" }} options={referralOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||||
@@ -272,19 +268,21 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
showSearch={{
|
showSearch={{
|
||||||
optionFilterProp: 'label',
|
optionFilterProp: "label",
|
||||||
filterOption: (input, option) =>
|
filterOption: (input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
|
||||||
}}
|
}}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
|
|
||||||
options={csrOptions}
|
options={csrOptions}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bodyshop.enforce_conversion_category && (
|
{bodyshop.enforce_conversion_category && (
|
||||||
<Form.Item name="category" label={t("jobs.fields.category")} rules={[{ required: bodyshop.enforce_conversion_category }]}>
|
<Form.Item
|
||||||
|
name="category"
|
||||||
|
label={t("jobs.fields.category")}
|
||||||
|
rules={[{ required: bodyshop.enforce_conversion_category }]}
|
||||||
|
>
|
||||||
<Select allowClear options={categoryOptions} />
|
<Select allowClear options={categoryOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -193,6 +193,9 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
|
<Form.Item label={t("jobs.fields.referralsource")} name="referral_source">
|
||||||
<Select
|
<Select
|
||||||
|
showSearch={{
|
||||||
|
optionFilterProp: "label"
|
||||||
|
}}
|
||||||
options={bodyshop.md_referral_sources.map((s) => ({
|
options={bodyshop.md_referral_sources.map((s) => ({
|
||||||
value: s,
|
value: s,
|
||||||
label: s
|
label: s
|
||||||
|
|||||||
@@ -43,19 +43,25 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||||
<Select disabled={jobRO} options={[
|
<Select
|
||||||
{ value: "W", label: t("jobs.labels.deductible.waived") },
|
disabled={jobRO}
|
||||||
{ value: "Y", label: t("jobs.labels.deductible.stands") }
|
options={[
|
||||||
]} />
|
{ value: "W", label: t("jobs.labels.deductible.waived") },
|
||||||
|
{ value: "Y", label: t("jobs.labels.deductible.stands") }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
<CurrencyInput disabled={jobRO} min={0} />
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_note")} name="ded_note">
|
<Form.Item label={t("jobs.fields.ded_note")} name="ded_note">
|
||||||
<Select disabled={jobRO} options={bodyshop.md_ded_notes.map((n) => ({
|
<Select
|
||||||
value: n,
|
disabled={jobRO}
|
||||||
label: n
|
options={bodyshop.md_ded_notes.map((n) => ({
|
||||||
}))} />
|
value: n,
|
||||||
|
label: n
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
@@ -65,10 +71,14 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||||
<Select disabled={jobRO} onChange={handleInsCoChange} options={bodyshop.md_ins_cos.map((s) => ({
|
<Select
|
||||||
value: s.name,
|
disabled={jobRO}
|
||||||
label: s.name
|
onChange={handleInsCoChange}
|
||||||
}))} />
|
options={bodyshop.md_ins_cos.map((s) => ({
|
||||||
|
value: s.name,
|
||||||
|
label: s.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
@@ -119,19 +129,30 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select disabled={jobRO} allowClear options={bodyshop.md_referral_sources.map((s) => ({
|
<Select
|
||||||
value: s,
|
disabled={jobRO}
|
||||||
label: s
|
allowClear
|
||||||
}))} />
|
showSearch={{
|
||||||
|
optionFilterProp: "label"
|
||||||
|
}}
|
||||||
|
options={bodyshop.md_referral_sources.map((s) => ({
|
||||||
|
value: s,
|
||||||
|
label: s
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
<Form.Item label={t("jobs.fields.referral_source_extra")} name="referral_source_extra">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
|
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
|
||||||
<Select disabled={jobRO} allowClear options={bodyshop.appt_alt_transport.map((s) => ({
|
<Select
|
||||||
value: s,
|
disabled={jobRO}
|
||||||
label: s
|
allowClear
|
||||||
}))} />
|
options={bodyshop.appt_alt_transport.map((s) => ({
|
||||||
|
value: s,
|
||||||
|
label: s
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
@@ -233,10 +254,14 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow header={t("jobs.forms.other")}>
|
<FormRow header={t("jobs.forms.other")}>
|
||||||
<Form.Item label={t("jobs.fields.category")} name="category">
|
<Form.Item label={t("jobs.fields.category")} name="category">
|
||||||
<Select disabled={jobRO} allowClear options={bodyshop.md_categories.map((s) => ({
|
<Select
|
||||||
value: s,
|
disabled={jobRO}
|
||||||
label: s
|
allowClear
|
||||||
}))} />
|
options={bodyshop.md_categories.map((s) => ({
|
||||||
|
value: s,
|
||||||
|
label: s
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
|
<Form.Item label={t("jobs.fields.selling_dealer")} name="selling_dealer">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Card, Col, Row, Space } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { isFunction } from "lodash";
|
import { isFunction } from "lodash";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Lightbox from "react-image-lightbox";
|
import Lightbox from "react-image-lightbox";
|
||||||
import "react-image-lightbox/style.css";
|
import "react-image-lightbox/style.css";
|
||||||
@@ -12,12 +12,12 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
|
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
|
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
|
||||||
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
||||||
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
||||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
||||||
import LocalMediaGrid from "../jobs-documents-local-gallery/local-media-grid.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -38,6 +38,9 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
const [galleryImages, setGalleryImages] = useState({ images: [], other: [] });
|
const [galleryImages, setGalleryImages] = useState({ images: [], other: [] });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||||
|
const [previewUrls, setPreviewUrls] = useState({});
|
||||||
|
const [previewError, setPreviewError] = useState(null);
|
||||||
|
const previewUrlsRef = useRef({});
|
||||||
|
|
||||||
const fetchThumbnails = useCallback(() => {
|
const fetchThumbnails = useCallback(() => {
|
||||||
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
|
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId, billId });
|
||||||
@@ -49,8 +52,86 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
}
|
}
|
||||||
}, [data, fetchThumbnails]);
|
}, [data, fetchThumbnails]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
Object.values(previewUrlsRef.current).forEach(URL.revokeObjectURL);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedImage = modalState.open ? galleryImages.images[modalState.index] : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modalState.open || !selectedImage?.id) return;
|
||||||
|
|
||||||
|
if (previewUrlsRef.current[selectedImage.id]) {
|
||||||
|
setPreviewError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
async function loadPreviewImage() {
|
||||||
|
setPreviewError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
"/media/imgproxy/original",
|
||||||
|
{ documentId: selectedImage.id },
|
||||||
|
{
|
||||||
|
responseType: "blob",
|
||||||
|
signal: controller.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const blobUrl = URL.createObjectURL(response.data);
|
||||||
|
|
||||||
|
previewUrlsRef.current = {
|
||||||
|
...previewUrlsRef.current,
|
||||||
|
[selectedImage.id]: blobUrl
|
||||||
|
};
|
||||||
|
setPreviewUrls(previewUrlsRef.current);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isCancel?.(error) || error.name === "CanceledError") return;
|
||||||
|
|
||||||
|
console.error("Failed to fetch original image blob", error);
|
||||||
|
setPreviewError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPreviewImage();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [modalState.open, selectedImage?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalState.open && !selectedImage) {
|
||||||
|
setModalState({ open: false, index: 0 });
|
||||||
|
}
|
||||||
|
}, [modalState.open, selectedImage]);
|
||||||
|
|
||||||
|
const openEditorForImage = useCallback((image) => {
|
||||||
|
if (!image?.id) return;
|
||||||
|
|
||||||
|
const newWindow = window.open(
|
||||||
|
`${window.location.protocol}//${window.location.host}/edit?documentId=${image.id}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
);
|
||||||
|
if (newWindow) newWindow.opener = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||||
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||||
|
const previewSrc = selectedImage ? previewUrls[selectedImage.id] : null;
|
||||||
|
const getLightboxImageSrc = useCallback(
|
||||||
|
(index) => {
|
||||||
|
const image = galleryImages.images[index];
|
||||||
|
return image ? previewUrls[image.id] || image.src : undefined;
|
||||||
|
},
|
||||||
|
[galleryImages.images, previewUrls]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
@@ -147,30 +228,33 @@ function JobsDocumentsImgproxyComponent({
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
{modalState.open && (
|
{modalState.open && selectedImage && (
|
||||||
<Lightbox
|
<Lightbox
|
||||||
toolbarButtons={[
|
toolbarButtons={[
|
||||||
<EditFilled
|
<EditFilled
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newWindow = window.open(
|
openEditorForImage(selectedImage);
|
||||||
`${window.location.protocol}//${window.location.host}/edit?documentId=${
|
|
||||||
galleryImages.images[modalState.index].id
|
|
||||||
}`,
|
|
||||||
"_blank",
|
|
||||||
"noopener,noreferrer"
|
|
||||||
);
|
|
||||||
if (newWindow) newWindow.opener = null;
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
]}
|
]}
|
||||||
mainSrc={galleryImages.images[modalState.index].fullsize}
|
imageLoadErrorMessage={previewError ? t("general.errors.notfound") : undefined}
|
||||||
nextSrc={galleryImages.images[(modalState.index + 1) % galleryImages.images.length].fullsize}
|
mainSrc={previewSrc || selectedImage.src}
|
||||||
prevSrc={
|
mainSrcThumbnail={selectedImage.src}
|
||||||
|
nextSrc={getLightboxImageSrc((modalState.index + 1) % galleryImages.images.length)}
|
||||||
|
nextSrcThumbnail={galleryImages.images[(modalState.index + 1) % galleryImages.images.length]?.src}
|
||||||
|
prevSrc={getLightboxImageSrc(
|
||||||
|
(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length
|
||||||
|
)}
|
||||||
|
prevSrcThumbnail={
|
||||||
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
|
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
|
||||||
.fullsize
|
?.src
|
||||||
}
|
}
|
||||||
onCloseRequest={() => setModalState({ open: false, index: 0 })}
|
reactModalProps={{ ariaHideApp: false }}
|
||||||
|
onCloseRequest={() => {
|
||||||
|
setModalState({ open: false, index: 0 });
|
||||||
|
setPreviewError(null);
|
||||||
|
}}
|
||||||
onMovePrevRequest={() =>
|
onMovePrevRequest={() =>
|
||||||
setModalState({
|
setModalState({
|
||||||
...modalState,
|
...modalState,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
|
||||||
import { Checkbox, Form } from "antd";
|
import { Checkbox, Form } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
@@ -9,18 +8,18 @@ import PropTypes from "prop-types";
|
|||||||
* @param form
|
* @param form
|
||||||
* @param disabled
|
* @param disabled
|
||||||
* @param onHeaderChange
|
* @param onHeaderChange
|
||||||
|
* @param scenarioKeys
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange }) => {
|
const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange, scenarioKeys }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Subscribe to all form values so that this component re-renders on changes.
|
// Subscribe to all form values so that this component re-renders on changes.
|
||||||
const formValues = Form.useWatch([], form) || {};
|
const formValues = Form.useWatch([], form) || {};
|
||||||
|
|
||||||
// Determine if all scenarios for this channel are checked.
|
// Determine if all scenarios for this channel are checked.
|
||||||
const allChecked =
|
const allChecked = scenarioKeys.length > 0 && scenarioKeys.every((scenario) => formValues[scenario]?.[channel]);
|
||||||
notificationScenarios.length > 0 && notificationScenarios.every((scenario) => formValues[scenario]?.[channel]);
|
|
||||||
|
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
const checked = e.target.checked;
|
const checked = e.target.checked;
|
||||||
@@ -28,7 +27,7 @@ const ColumnHeaderCheckbox = ({ channel, form, disabled = false, onHeaderChange
|
|||||||
const currentValues = form.getFieldsValue();
|
const currentValues = form.getFieldsValue();
|
||||||
// Update each scenario for this channel.
|
// Update each scenario for this channel.
|
||||||
const newValues = { ...currentValues };
|
const newValues = { ...currentValues };
|
||||||
notificationScenarios.forEach((scenario) => {
|
scenarioKeys.forEach((scenario) => {
|
||||||
newValues[scenario] = { ...newValues[scenario], [channel]: checked };
|
newValues[scenario] = { ...newValues[scenario], [channel]: checked };
|
||||||
});
|
});
|
||||||
// Update form values.
|
// Update form values.
|
||||||
@@ -50,7 +49,8 @@ ColumnHeaderCheckbox.propTypes = {
|
|||||||
channel: PropTypes.oneOf(["app", "email", "fcm"]).isRequired,
|
channel: PropTypes.oneOf(["app", "email", "fcm"]).isRequired,
|
||||||
form: PropTypes.object.isRequired,
|
form: PropTypes.object.isRequired,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
onHeaderChange: PropTypes.func
|
onHeaderChange: PropTypes.func,
|
||||||
|
scenarioKeys: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColumnHeaderCheckbox;
|
export default ColumnHeaderCheckbox;
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import {
|
|||||||
UPDATE_NOTIFICATION_SETTINGS,
|
UPDATE_NOTIFICATION_SETTINGS,
|
||||||
UPDATE_NOTIFICATIONS_AUTOADD
|
UPDATE_NOTIFICATIONS_AUTOADD
|
||||||
} from "../../graphql/user.queries.js";
|
} from "../../graphql/user.queries.js";
|
||||||
import { notificationScenarios } from "../../utils/jobNotificationScenarios.js";
|
import { getNotificationScenarios, notificationScenarioDefaults } from "../../utils/jobNotificationScenarios.js";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Settings Form
|
* Notifications Settings Form
|
||||||
@@ -35,6 +36,7 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
const notificationScenarios = getNotificationScenarios({ includeEsign: hasDocumensoApiKey(bodyshop) });
|
||||||
|
|
||||||
// Fetch notification settings and notifications_autoadd
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
@@ -55,7 +57,8 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
|
|
||||||
// Ensure each scenario has an object with { app, email, fcm }
|
// Ensure each scenario has an object with { app, email, fcm }
|
||||||
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
const formattedValues = notificationScenarios.reduce((acc, scenario) => {
|
||||||
acc[scenario] = settings[scenario] ?? { app: false, email: false, fcm: false };
|
acc[scenario] = settings[scenario] ??
|
||||||
|
notificationScenarioDefaults[scenario] ?? { app: false, email: false, fcm: false };
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
setInitialAutoAdd(autoAdd);
|
setInitialAutoAdd(autoAdd);
|
||||||
setIsDirty(false); // Reset dirty state when new data loads
|
setIsDirty(false); // Reset dirty state when new data loads
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form, notificationScenarios]);
|
||||||
|
|
||||||
// Handle toggle of notifications_autoadd
|
// Handle toggle of notifications_autoadd
|
||||||
const handleAutoAddToggle = async (checked) => {
|
const handleAutoAddToggle = async (checked) => {
|
||||||
@@ -136,7 +139,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
width: "80%"
|
width: "80%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: <ColumnHeaderCheckbox channel="app" form={form} onHeaderChange={() => setIsDirty(true)} />,
|
title: (
|
||||||
|
<ColumnHeaderCheckbox
|
||||||
|
channel="app"
|
||||||
|
form={form}
|
||||||
|
onHeaderChange={() => setIsDirty(true)}
|
||||||
|
scenarioKeys={notificationScenarios}
|
||||||
|
/>
|
||||||
|
),
|
||||||
dataIndex: "app",
|
dataIndex: "app",
|
||||||
key: "app",
|
key: "app",
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -147,7 +157,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: <ColumnHeaderCheckbox channel="email" form={form} onHeaderChange={() => setIsDirty(true)} />,
|
title: (
|
||||||
|
<ColumnHeaderCheckbox
|
||||||
|
channel="email"
|
||||||
|
form={form}
|
||||||
|
onHeaderChange={() => setIsDirty(true)}
|
||||||
|
scenarioKeys={notificationScenarios}
|
||||||
|
/>
|
||||||
|
),
|
||||||
dataIndex: "email",
|
dataIndex: "email",
|
||||||
key: "email",
|
key: "email",
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -162,7 +179,14 @@ const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
|||||||
// Currently disabled for prod
|
// Currently disabled for prod
|
||||||
if (!import.meta.env.PROD) {
|
if (!import.meta.env.PROD) {
|
||||||
columns.push({
|
columns.push({
|
||||||
title: <ColumnHeaderCheckbox channel="fcm" form={form} onHeaderChange={() => setIsDirty(true)} />,
|
title: (
|
||||||
|
<ColumnHeaderCheckbox
|
||||||
|
channel="fcm"
|
||||||
|
form={form}
|
||||||
|
onHeaderChange={() => setIsDirty(true)}
|
||||||
|
scenarioKeys={notificationScenarios}
|
||||||
|
/>
|
||||||
|
),
|
||||||
dataIndex: "fcm",
|
dataIndex: "fcm",
|
||||||
key: "fcm",
|
key: "fcm",
|
||||||
align: "center",
|
align: "center",
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import ResponsiveTable from "../responsive-table/responsive-table.component";
|
|||||||
|
|
||||||
export default function OwnersListComponent({ loading, owners, total, refetch }) {
|
export default function OwnersListComponent({ loading, owners, total, refetch }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const {
|
const { page, pageSize } = search;
|
||||||
page
|
|
||||||
// sortcolumn, sortorder
|
|
||||||
} = search;
|
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
@@ -71,10 +72,14 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
const nextPageSize = pagination?.pageSize || currentPageSize;
|
||||||
|
const pageSizeChanged = nextPageSize !== currentPageSize;
|
||||||
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
const updatedSearch = {
|
const updatedSearch = {
|
||||||
...search,
|
...search,
|
||||||
page: pagination.current,
|
pageSize: nextPageSize,
|
||||||
|
page: pageSizeChanged ? 1 : pagination.current,
|
||||||
sortcolumn: sorter.columnKey,
|
sortcolumn: sorter.columnKey,
|
||||||
sortorder: sorter.order
|
sortorder: sorter.order
|
||||||
};
|
};
|
||||||
@@ -119,7 +124,7 @@ export default function OwnersListComponent({ loading, owners, total, refetch })
|
|||||||
>
|
>
|
||||||
<ResponsiveTable
|
<ResponsiveTable
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1, 10), total: total }}
|
pagination={{ placement: "top", pageSize: currentPageSize, current: currentPage, showSizeChanger: true, total: total }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
mobileColumnKeys={["name", "ownr_ph1", "ownr_ph2"]}
|
mobileColumnKeys={["name", "ownr_ph1", "ownr_ph2"]}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ import { pageLimit } from "../../utils/config";
|
|||||||
|
|
||||||
export default function OwnersListContainer() {
|
export default function OwnersListContainer() {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const { page, sortcolumn, sortorder, search } = searchParams;
|
const { page, sortcolumn, sortorder, search, pageSize } = searchParams;
|
||||||
|
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_OWNERS_PAGINATED, {
|
const { loading, error, data, refetch } = useQuery(QUERY_ALL_OWNERS_PAGINATED, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
search: search || "",
|
search: search || "",
|
||||||
offset: page ? (page - 1) * pageLimit : 0,
|
offset: (currentPage - 1) * currentPageSize,
|
||||||
limit: pageLimit,
|
limit: currentPageSize,
|
||||||
order: [
|
order: [
|
||||||
{
|
{
|
||||||
[sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
[sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
||||||
|
|||||||
@@ -176,6 +176,9 @@ export function PartsOrderModalComponent({
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item key={`${index}job_line_id`} name={[field.name, "job_line_id"]} hidden>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("parts_orders.fields.line_remarks")}
|
label={t("parts_orders.fields.line_remarks")}
|
||||||
key={`${index}line_remarks`}
|
key={`${index}line_remarks`}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { buildSubmittedPartsOrderLines, getSubmittedPartsOrderJobLineIds } from "./parts-order-modal.utils.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -82,15 +83,10 @@ export function PartsOrderModalContainer({
|
|||||||
|
|
||||||
// Force job_line_id from context so it never gets dropped by AntD form submission behavior.
|
// Force job_line_id from context so it never gets dropped by AntD form submission behavior.
|
||||||
const submittedLines = values?.parts_order_lines?.data ?? [];
|
const submittedLines = values?.parts_order_lines?.data ?? [];
|
||||||
const forcedLines = submittedLines.map((p, index) => {
|
const forcedLines = buildSubmittedPartsOrderLines({
|
||||||
const originalLine = linesToOrder?.[index];
|
submittedLines,
|
||||||
const jobLineId = isReturn ? originalLine?.joblineid : originalLine?.id;
|
linesToOrder,
|
||||||
|
isReturn
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
job_line_id: jobLineId,
|
|
||||||
...(isReturn && { cm_received: false })
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let insertResult;
|
let insertResult;
|
||||||
@@ -147,10 +143,7 @@ export function PartsOrderModalContainer({
|
|||||||
type: isReturn ? "jobspartsreturn" : "jobspartsorder"
|
type: isReturn ? "jobspartsreturn" : "jobspartsorder"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use linesToOrder from context instead of form values to preserve job line ids
|
const jobLineIds = getSubmittedPartsOrderJobLineIds(forcedLines);
|
||||||
const jobLineIds = (linesToOrder ?? [])
|
|
||||||
.filter((line) => (isReturn ? line.joblineid : line.id))
|
|
||||||
.map((line) => (isReturn ? line.joblineid : line.id));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jobLinesResult = await updateJobLines({
|
const jobLinesResult = await updateJobLines({
|
||||||
@@ -206,23 +199,20 @@ export function PartsOrderModalContainer({
|
|||||||
isinhouse: true,
|
isinhouse: true,
|
||||||
date: dayjs(),
|
date: dayjs(),
|
||||||
total: 0,
|
total: 0,
|
||||||
billlines: forcedLines.map((p, index) => {
|
billlines: forcedLines.map((p) => ({
|
||||||
const originalLine = linesToOrder?.[index];
|
joblineid: p.job_line_id,
|
||||||
return {
|
actual_price: p.act_price,
|
||||||
joblineid: isReturn ? originalLine?.joblineid : originalLine?.id,
|
actual_cost: 0, // p.act_price,
|
||||||
actual_price: p.act_price,
|
line_desc: p.line_desc,
|
||||||
actual_cost: 0, // p.act_price,
|
line_remarks: p.line_remarks,
|
||||||
line_desc: p.line_desc,
|
part_type: p.part_type,
|
||||||
line_remarks: p.line_remarks,
|
quantity: p.quantity || 1,
|
||||||
part_type: p.part_type,
|
applicable_taxes: {
|
||||||
quantity: p.quantity || 1,
|
local: false,
|
||||||
applicable_taxes: {
|
state: false,
|
||||||
local: false,
|
federal: false
|
||||||
state: false,
|
}
|
||||||
federal: false
|
}))
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const getPartsOrderJobLineId = ({ line, originalLine, isReturn }) => {
|
||||||
|
return line?.job_line_id || (isReturn ? originalLine?.joblineid : originalLine?.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildSubmittedPartsOrderLines = ({ submittedLines = [], linesToOrder = [], isReturn }) => {
|
||||||
|
return submittedLines.map((line, index) => {
|
||||||
|
const jobLineId = getPartsOrderJobLineId({
|
||||||
|
line,
|
||||||
|
originalLine: linesToOrder?.[index],
|
||||||
|
isReturn
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
job_line_id: jobLineId,
|
||||||
|
...(isReturn && { cm_received: false })
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubmittedPartsOrderJobLineIds = (partsOrderLines = []) => {
|
||||||
|
return partsOrderLines.map((line) => line.job_line_id).filter(Boolean);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildSubmittedPartsOrderLines, getSubmittedPartsOrderJobLineIds } from "./parts-order-modal.utils.js";
|
||||||
|
|
||||||
|
describe("parts order modal utilities", () => {
|
||||||
|
it("preserves submitted job line ids after a row is removed", () => {
|
||||||
|
const submittedLines = [
|
||||||
|
{ line_desc: "second line", job_line_id: "job-line-2" },
|
||||||
|
{ line_desc: "third line", job_line_id: "job-line-3" }
|
||||||
|
];
|
||||||
|
const linesToOrder = [{ id: "job-line-1" }, { id: "job-line-2" }, { id: "job-line-3" }];
|
||||||
|
|
||||||
|
const result = buildSubmittedPartsOrderLines({ submittedLines, linesToOrder, isReturn: false });
|
||||||
|
|
||||||
|
expect(result.map((line) => line.job_line_id)).toEqual(["job-line-2", "job-line-3"]);
|
||||||
|
expect(getSubmittedPartsOrderJobLineIds(result)).toEqual(["job-line-2", "job-line-3"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to original return line ids when the form omits hidden metadata", () => {
|
||||||
|
const submittedLines = [{ line_desc: "return line" }];
|
||||||
|
const linesToOrder = [{ joblineid: "return-job-line-1" }];
|
||||||
|
|
||||||
|
const result = buildSubmittedPartsOrderLines({ submittedLines, linesToOrder, isReturn: true });
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
line_desc: "return line",
|
||||||
|
job_line_id: "return-job-line-1",
|
||||||
|
cm_received: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -29,7 +29,10 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function PartsQueueListComponent({ bodyshop }) {
|
export function PartsQueueListComponent({ bodyshop }) {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
|
const { selected, sortcolumn, sortorder, statusFilters, page, pageSize } = searchParams;
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
||||||
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
|
const [viewTimeStamp, setViewTimeStamp] = useLocalStorage("parts_queue_timestamps", false);
|
||||||
@@ -66,7 +69,11 @@ export function PartsQueueListComponent({ bodyshop }) {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
// searchParams.page = pagination.current;
|
const nextPageSize = pagination?.pageSize || currentPageSize;
|
||||||
|
const pageSizeChanged = nextPageSize !== currentPageSize;
|
||||||
|
|
||||||
|
searchParams.pageSize = nextPageSize;
|
||||||
|
searchParams.page = pageSizeChanged ? 1 : pagination.current;
|
||||||
searchParams.sortcolumn = sorter.columnKey;
|
searchParams.sortcolumn = sorter.columnKey;
|
||||||
searchParams.sortorder = sorter.order;
|
searchParams.sortorder = sorter.order;
|
||||||
|
|
||||||
@@ -315,9 +322,10 @@ export function PartsQueueListComponent({ bodyshop }) {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
placement: "top",
|
placement: "top",
|
||||||
pageSize: pageLimit
|
pageSize: currentPageSize,
|
||||||
// current: parseInt(page || 1),
|
current: currentPage,
|
||||||
// total: data && data.jobs_aggregate.aggregate.count,
|
showSizeChanger: true,
|
||||||
|
total: jobs.length
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
mobileColumnKeys={["ro_number", "ownr_ln", "status", "vehicle", "partsstatus"]}
|
mobileColumnKeys={["ro_number", "ownr_ln", "status", "vehicle", "partsstatus"]}
|
||||||
|
|||||||
@@ -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,9 @@ 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";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
printCenterModal: selectPrintCenter,
|
printCenterModal: selectPrintCenter,
|
||||||
@@ -17,12 +20,29 @@ 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();
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
|
|
||||||
const renderToNewWindow = async () => {
|
const renderToNewWindow = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -39,6 +59,30 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const esignatureGenerate = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { token, documentId, envelopeId }
|
||||||
|
} = await axios.post("/esign/new", {
|
||||||
|
name: item.key,
|
||||||
|
jobid: id,
|
||||||
|
context,
|
||||||
|
bodyshop,
|
||||||
|
templateObject: {
|
||||||
|
name: item.key,
|
||||||
|
variables: { id: id }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setEsignatureContext({ context: { token, documentId, envelopeId, 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 +98,7 @@ export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop,
|
|||||||
<li>
|
<li>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{item.title}
|
{item.title}
|
||||||
|
{esignatureEnabled && <SignatureFilled onClick={esignatureGenerate} />}
|
||||||
<PrinterOutlined onClick={renderToNewWindow} />
|
<PrinterOutlined onClick={renderToNewWindow} />
|
||||||
{!technician ? (
|
{!technician ? (
|
||||||
<MailOutlined
|
<MailOutlined
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { selectPrintCenter } from "../../redux/modals/modals.selectors";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
|
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
|
||||||
|
import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component";
|
||||||
import PrintCenterItem from "../print-center-item/print-center-item.component";
|
import PrintCenterItem from "../print-center-item/print-center-item.component";
|
||||||
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
|
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
|
||||||
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
|
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
|
||||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils";
|
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
printCenterModal: selectPrintCenter,
|
printCenterModal: selectPrintCenter,
|
||||||
@@ -36,6 +38,9 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
||||||
|
const dmsMode = getDmsMode(bodyshop, "off");
|
||||||
|
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
|
|
||||||
const Templates = !hasDMSKey
|
const Templates = !hasDMSKey
|
||||||
? Object.keys(tempList)
|
? Object.keys(tempList)
|
||||||
@@ -60,8 +65,9 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
||||||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
|
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
|
||||||
)
|
)
|
||||||
|
.filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode))
|
||||||
.filter((temp) => !technician || temp.group !== "financial");
|
.filter((temp) => !technician || temp.group !== "financial");
|
||||||
|
|
||||||
const JobsReportsList =
|
const JobsReportsList =
|
||||||
Enhanced_Payroll.treatment === "on"
|
Enhanced_Payroll.treatment === "on"
|
||||||
? Object.keys(Templates)
|
? Object.keys(Templates)
|
||||||
@@ -94,6 +100,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PrintCenterJobsLabels jobId={jobId} />
|
<PrintCenterJobsLabels jobId={jobId} />
|
||||||
|
{esignatureEnabled && <EsignatureCustomDocument jobId={jobId} />}
|
||||||
<Jobd3RdPartyModal jobId={jobId} job={job} />
|
<Jobd3RdPartyModal jobId={jobId} job={job} />
|
||||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
|
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -80,14 +80,14 @@ const ModelInfoToolTip = ({ metadata, cardSettings }) =>
|
|||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<EllipsesToolTip
|
<EllipsesToolTip
|
||||||
title={
|
title={
|
||||||
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
|
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc || metadata.v_color
|
||||||
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
? `${metadata.v_model_yr || ""} ${metadata.v_color || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
kiosk={cardSettings.kiosk}
|
kiosk={cardSettings.kiosk}
|
||||||
>
|
>
|
||||||
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
|
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc || metadata.v_color ? (
|
||||||
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
`${metadata.v_model_yr || ""} ${metadata.v_color || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||||
) : (
|
) : (
|
||||||
<span> </span>
|
<span> </span>
|
||||||
)}
|
)}
|
||||||
@@ -431,6 +431,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
|
|||||||
<Card
|
<Card
|
||||||
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||||
size="small"
|
size="small"
|
||||||
|
styles={{ header: { backgroundColor: "var(--card-bg-fallback)" } }}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: cardSettings?.cardcolor
|
backgroundColor: cardSettings?.cardcolor
|
||||||
? bgColor.fallback || `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a || 1})`
|
? bgColor.fallback || `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a || 1})`
|
||||||
|
|||||||
@@ -28,11 +28,14 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const calculateTotal = (items, key, subKey) => {
|
const calculateTotal = (items, key, subKey) => {
|
||||||
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
|
return items.reduce((acc, item) => acc + (item?.[key]?.aggregate?.sum?.[subKey] ?? 0), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTotalAmount = (items, key) => {
|
const calculateTotalAmount = (items, key) => {
|
||||||
return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
|
return items.reduce(
|
||||||
|
(acc, item) => acc.add(Dinero(item?.[key]?.totals?.subtotal ?? Dinero())),
|
||||||
|
Dinero({ amount: 0 })
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateReducerTotalAmount = (lanes, key) => {
|
const calculateReducerTotalAmount = (lanes, key) => {
|
||||||
@@ -67,58 +70,83 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredData = cardSettings.excludeSuspended === true ? data.filter((item) => item.suspended !== true) : data;
|
||||||
|
const filteredReducerData =
|
||||||
|
cardSettings.excludeSuspended === true
|
||||||
|
? {
|
||||||
|
...reducerData,
|
||||||
|
lanes: reducerData.lanes.map((lane) => ({
|
||||||
|
...lane,
|
||||||
|
cards: lane.cards.filter((card) => card.metadata.suspended !== true)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
: reducerData;
|
||||||
|
|
||||||
const totalHrs = cardSettings.totalHrs
|
const totalHrs = cardSettings.totalHrs
|
||||||
? parseFloat((calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs")).toFixed(2))
|
? parseFloat(
|
||||||
|
(
|
||||||
|
calculateTotal(filteredData, "labhrs", "mod_lb_hrs") + calculateTotal(filteredData, "larhrs", "mod_lb_hrs")
|
||||||
|
).toFixed(2)
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const totalLAB = cardSettings.totalLAB
|
const totalLAB = cardSettings.totalLAB
|
||||||
? parseFloat(calculateTotal(data, "labhrs", "mod_lb_hrs").toFixed(2))
|
? parseFloat(calculateTotal(filteredData, "labhrs", "mod_lb_hrs").toFixed(2))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const totalLAR = cardSettings.totalLAR
|
const totalLAR = cardSettings.totalLAR
|
||||||
? parseFloat(calculateTotal(data, "larhrs", "mod_lb_hrs").toFixed(2))
|
? parseFloat(calculateTotal(filteredData, "larhrs", "mod_lb_hrs").toFixed(2))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const jobsInProduction = cardSettings.jobsInProduction ? data.length : null;
|
const jobsInProduction = cardSettings.jobsInProduction ? filteredData.length : null;
|
||||||
|
|
||||||
const totalAmountInProduction = cardSettings.totalAmountInProduction
|
const totalAmountInProduction = cardSettings.totalAmountInProduction
|
||||||
? calculateTotalAmount(data, "job_totals").toFormat("$0,0.00")
|
? calculateTotalAmount(filteredData, "job_totals").toFormat("$0,0.00")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const totalAmountOnBoard = reducerData && cardSettings.totalAmountOnBoard
|
const totalAmountOnBoard =
|
||||||
? calculateReducerTotalAmount(reducerData.lanes, "job_totals").toFormat("$0,0.00")
|
filteredReducerData && cardSettings.totalAmountOnBoard
|
||||||
: null;
|
? calculateReducerTotalAmount(filteredReducerData.lanes, "job_totals").toFormat("$0,0.00")
|
||||||
|
: null;
|
||||||
|
|
||||||
const totalHrsOnBoard = reducerData && cardSettings.totalHrsOnBoard
|
const totalHrsOnBoard =
|
||||||
? parseFloat((
|
filteredReducerData && cardSettings.totalHrsOnBoard
|
||||||
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
|
? parseFloat(
|
||||||
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs")
|
(
|
||||||
).toFixed(2))
|
calculateReducerTotal(filteredReducerData.lanes, "labhrs", "mod_lb_hrs") +
|
||||||
: null;
|
calculateReducerTotal(filteredReducerData.lanes, "larhrs", "mod_lb_hrs")
|
||||||
|
).toFixed(2)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
const totalLABOnBoard = reducerData && cardSettings.totalLABOnBoard
|
const totalLABOnBoard =
|
||||||
? parseFloat(calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs").toFixed(2))
|
filteredReducerData && cardSettings.totalLABOnBoard
|
||||||
: null;
|
? parseFloat(calculateReducerTotal(filteredReducerData.lanes, "labhrs", "mod_lb_hrs").toFixed(2))
|
||||||
|
: null;
|
||||||
|
|
||||||
const totalLAROnBoard = reducerData && cardSettings.totalLAROnBoard
|
const totalLAROnBoard =
|
||||||
? parseFloat(calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs").toFixed(2))
|
filteredReducerData && cardSettings.totalLAROnBoard
|
||||||
: null;
|
? parseFloat(calculateReducerTotal(filteredReducerData.lanes, "larhrs", "mod_lb_hrs").toFixed(2))
|
||||||
|
: null;
|
||||||
|
|
||||||
const jobsOnBoard = reducerData && cardSettings.jobsOnBoard
|
const jobsOnBoard =
|
||||||
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
filteredReducerData && cardSettings.jobsOnBoard
|
||||||
: null;
|
? filteredReducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
||||||
|
: null;
|
||||||
|
|
||||||
const tasksInProduction = cardSettings.tasksInProduction
|
const tasksInProduction = cardSettings.tasksInProduction
|
||||||
? data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0)
|
? filteredData.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const tasksOnBoard = reducerData && cardSettings.tasksOnBoard
|
const tasksOnBoard =
|
||||||
? reducerData.lanes.reduce((acc, lane) => {
|
filteredReducerData && cardSettings.tasksOnBoard
|
||||||
return (
|
? filteredReducerData.lanes.reduce((acc, lane) => {
|
||||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
return (
|
||||||
);
|
acc +
|
||||||
}, 0)
|
lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||||
: null;
|
);
|
||||||
|
}, 0)
|
||||||
|
: null;
|
||||||
|
|
||||||
const statistics = mergeStatistics(statisticsItems, [
|
const statistics = mergeStatistics(statisticsItems, [
|
||||||
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChan
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t("production.settings.statistics_title")}>
|
<Card
|
||||||
|
title={t("production.settings.statistics_title")}
|
||||||
|
extra={
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Form.Item name="excludeSuspended" valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||||
|
<Checkbox>{t("production.settings.statistics.exclude_suspended")}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable direction="grid" droppableId="statistics">
|
<Droppable direction="grid" droppableId="statistics">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ const defaultKanbanSettings = {
|
|||||||
subtotal: false,
|
subtotal: false,
|
||||||
statisticsOrder: statisticsItems.map((item) => item.id),
|
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||||
selectedMdInsCos: [],
|
selectedMdInsCos: [],
|
||||||
selectedEstimators: []
|
selectedEstimators: [],
|
||||||
|
excludeSuspended: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultFilters = { search: "", employeeId: null, alert: false };
|
const defaultFilters = { search: "", employeeId: null, alert: false };
|
||||||
|
|||||||
@@ -140,13 +140,11 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
|||||||
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
technician ? (
|
technician ? (
|
||||||
<>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
|
<>{`${record.v_model_yr || ""} ${record.v_color || ""}${record.v_make_desc || ""} ${record.v_model_desc || ""} ${record.plate_no || ""}`}</>
|
||||||
record.v_color || ""
|
|
||||||
} ${record.plate_no || ""}`}</>
|
|
||||||
) : (
|
) : (
|
||||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
<Link
|
||||||
record.v_model_desc || ""
|
to={`/manage/vehicles/${record.vehicleid}`}
|
||||||
} ${record.v_color || ""} ${record.plate_no || ""}`}</Link>
|
>{`${record.v_model_yr || ""} ${record.v_color || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${record.plate_no || ""}`}</Link>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -621,7 +619,7 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
|||||||
sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order
|
sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: [])
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
export default productionListColumnsData;
|
export default productionListColumnsData;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
|||||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DatePickerRanges from "../../utils/DatePickerRanges";
|
import DatePickerRanges from "../../utils/DatePickerRanges";
|
||||||
|
import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
@@ -48,12 +49,18 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Templates = TemplateList("report_center");
|
const Templates = TemplateList("report_center");
|
||||||
|
const dmsMode = getDmsMode(bodyshop, "off");
|
||||||
|
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||||
const ReportsList = Object.keys(Templates)
|
const ReportsList = Object.keys(Templates)
|
||||||
.map((key) => Templates[key])
|
.map((key) => Templates[key])
|
||||||
.filter((temp) => {
|
.filter((temp) => {
|
||||||
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
|
const enhancedPayrollOn = Enhanced_Payroll.treatment === "on";
|
||||||
const adpPayrollOn = ADPPayroll.treatment === "on";
|
const adpPayrollOn = ADPPayroll.treatment === "on";
|
||||||
|
|
||||||
|
if (isReynoldsMode && temp.excludedDmsModes?.includes(dmsMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (enhancedPayrollOn && adpPayrollOn) {
|
if (enhancedPayrollOn && adpPayrollOn) {
|
||||||
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
|
return temp.enhanced_payroll !== false || temp.adp_payroll !== false;
|
||||||
}
|
}
|
||||||
@@ -408,6 +415,6 @@ const restrictedReports = [
|
|||||||
{ key: "job_costing_ro_estimator", days: 183 },
|
{ key: "job_costing_ro_estimator", days: 183 },
|
||||||
{ key: "job_lifecycle_date_detail", days: 183 },
|
{ key: "job_lifecycle_date_detail", days: 183 },
|
||||||
{ key: "job_lifecycle_date_summary", days: 183 },
|
{ key: "job_lifecycle_date_summary", days: 183 },
|
||||||
{ key: "customer_list", days: 183 },
|
{ key: "customer_list", days: 736 },
|
||||||
{ key: "customer_list_excel", days: 183 }
|
{ key: "customer_list_excel", days: 736 }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function ScheduleVerifyIntegrity({ currentUser }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentUser.email === "patrick@imex.prod")
|
if (currentUser.email === "allan@imex.prod" || currentUser.email === "dave@imex.prod")
|
||||||
return (
|
return (
|
||||||
<Button loading={loading} onClick={handleVerify}>
|
<Button loading={loading} onClick={handleVerify}>
|
||||||
Developer Use Only - Verify Schedule Integrity
|
Developer Use Only - Verify Schedule Integrity
|
||||||
|
|||||||
@@ -220,12 +220,16 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
|||||||
});
|
});
|
||||||
const savedEmployee = result?.data?.insert_employees?.returning?.[0];
|
const savedEmployee = result?.data?.insert_employees?.returning?.[0];
|
||||||
|
|
||||||
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
|
||||||
|
|
||||||
if (submitAction === "saveAndNew") {
|
if (submitAction === "saveAndNew") {
|
||||||
|
if (isNewEmployee) {
|
||||||
|
resetEmployeeFormToCurrentData();
|
||||||
|
}
|
||||||
navigateToEmployee("new");
|
navigateToEmployee("new");
|
||||||
} else if (savedEmployee?.id) {
|
} else if (savedEmployee?.id) {
|
||||||
|
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
||||||
navigateToEmployee(savedEmployee.id);
|
navigateToEmployee(savedEmployee.id);
|
||||||
|
} else {
|
||||||
|
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.success({
|
notification.success({
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { Form } from "antd";
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { DELETE_VACATION, INSERT_EMPLOYEES, QUERY_EMPLOYEE_BY_ID, UPDATE_EMPLOYEE } from "../../graphql/employees.queries";
|
import {
|
||||||
|
DELETE_VACATION,
|
||||||
|
INSERT_EMPLOYEES,
|
||||||
|
QUERY_EMPLOYEE_BY_ID,
|
||||||
|
UPDATE_EMPLOYEE
|
||||||
|
} from "../../graphql/employees.queries";
|
||||||
import { ShopEmployeesFormComponent } from "./shop-employees-form.component.jsx";
|
import { ShopEmployeesFormComponent } from "./shop-employees-form.component.jsx";
|
||||||
|
|
||||||
const insertEmployeesMock = vi.fn();
|
const insertEmployeesMock = vi.fn();
|
||||||
@@ -335,6 +340,15 @@ describe("ShopEmployeesFormComponent", () => {
|
|||||||
expect(formInstance.isFieldsTouched()).toBe(false);
|
expect(formInstance.isFieldsTouched()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("textbox", { name: "First Name" })).toHaveValue("");
|
||||||
|
expect(screen.getByRole("textbox", { name: "Last Name" })).toHaveValue("");
|
||||||
|
expect(screen.getByRole("textbox", { name: "Employee Number" })).toHaveValue("");
|
||||||
|
expect(screen.getByRole("textbox", { name: "PIN" })).toHaveValue("");
|
||||||
|
expect(screen.getByRole("textbox", { name: "Hire Date" })).toHaveValue("");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("New Employee")).toBeInTheDocument();
|
||||||
expect(navigateMock).toHaveBeenCalledWith({
|
expect(navigateMock).toHaveBeenCalledWith({
|
||||||
search: "employeeId=new"
|
search: "employeeId=new"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
|
|||||||
import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx";
|
import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx";
|
||||||
import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
|
import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js";
|
||||||
import {
|
import {
|
||||||
INLINE_TITLE_GROUP_STYLE,
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
INLINE_TITLE_HANDLE_STYLE,
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
@@ -25,16 +27,21 @@ import {
|
|||||||
|
|
||||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
const mapDispatchToProps = () => ({
|
const mapDispatchToProps = () => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
|
||||||
|
|
||||||
export function ShopInfoGeneral({ form }) {
|
export function ShopInfoGeneral({ form, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || [];
|
const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || [];
|
||||||
const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name");
|
const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name");
|
||||||
|
const hasDMSKey = bodyshop ? bodyshopHasDmsKey(bodyshop) : false;
|
||||||
|
const dmsMode = bodyshop ? getDmsMode(bodyshop, "off") : "none";
|
||||||
|
const isReynoldsMode = hasDMSKey && dmsMode === DMS_MAP.reynolds;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -174,7 +181,9 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
>
|
>
|
||||||
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}</div>
|
<div style={INLINE_TITLE_LABEL_STYLE}>
|
||||||
|
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
|
||||||
|
</div>
|
||||||
<Form.Item noStyle name={["scoreboard_target", "ignoreblockeddays"]} valuePropName="checked">
|
<Form.Item noStyle name={["scoreboard_target", "ignoreblockeddays"]} valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -311,7 +320,12 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} xl={8}>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<Form.Item key="use_fippa" label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
|
<Form.Item
|
||||||
|
key="use_fippa"
|
||||||
|
label={t("bodyshop.fields.use_fippa")}
|
||||||
|
name={["use_fippa"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -478,7 +492,12 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
<div style={INLINE_TITLE_LABEL_STYLE}>
|
<div style={INLINE_TITLE_LABEL_STYLE}>
|
||||||
{t("bodyshop.fields.system_settings.job_costing.use_paint_scale_data")}
|
{t("bodyshop.fields.system_settings.job_costing.use_paint_scale_data")}
|
||||||
</div>
|
</div>
|
||||||
<Form.Item noStyle key="use_paint_scale_data" name={["use_paint_scale_data"]} valuePropName="checked">
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
key="use_paint_scale_data"
|
||||||
|
name={["use_paint_scale_data"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,7 +577,12 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing" grow style={{ marginBottom: 0 }}>
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.shop_enabled_features")}
|
||||||
|
id="sharing"
|
||||||
|
grow
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("general.actions.sharetoteams")}
|
label={t("general.actions.sharetoteams")}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
@@ -566,6 +590,16 @@ export function ShopInfoGeneral({ form }) {
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{isReynoldsMode && (
|
||||||
|
<Form.Item
|
||||||
|
initialValue
|
||||||
|
label={t("bodyshop.fields.md_functionality_toggles.enhanced_early_ros")}
|
||||||
|
name={["md_functionality_toggles", "enhanced_early_ros"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -157,36 +157,36 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
</Col>
|
</Col>
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
ClosingPeriod.treatment === "on" && (
|
ClosingPeriod.treatment === "on" && (
|
||||||
<Col xs={24} sm={12} xl={8}>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="ClosingPeriod"
|
key="ClosingPeriod"
|
||||||
name={["accountingconfig", "ClosingPeriod"]}
|
name={["accountingconfig", "ClosingPeriod"]}
|
||||||
label={t("bodyshop.fields.closingperiod")}
|
label={t("bodyshop.fields.closingperiod")}
|
||||||
>
|
>
|
||||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
ADPPayroll.treatment === "on" && (
|
ADPPayroll.treatment === "on" && (
|
||||||
<Col xs={24} sm={12} xl={8}>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
key="companyCode"
|
key="companyCode"
|
||||||
name={["accountingconfig", "companyCode"]}
|
name={["accountingconfig", "companyCode"]}
|
||||||
label={t("bodyshop.fields.companycode")}
|
label={t("bodyshop.fields.companycode")}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
{HasFeatureAccess({ featureName: "export", bodyshop }) &&
|
||||||
ADPPayroll.treatment === "on" && (
|
ADPPayroll.treatment === "on" && (
|
||||||
<Col xs={24} sm={12} xl={8}>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
<Form.Item key="batchID" name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
<Form.Item key="batchID" name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
|
{HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && (
|
||||||
<>
|
<>
|
||||||
<Col xs={24} sm={12} xl={8}>
|
<Col xs={24} sm={12} xl={8}>
|
||||||
@@ -512,6 +512,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} suffix="%" />
|
<InputNumber min={0} max={100} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{bodyshop.cdk_dealerid && (
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.dms.disablecontact")}
|
||||||
|
valuePropName="checked"
|
||||||
|
name={["cdk_configuration", "disablecontact"]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
{bodyshop.pbs_serialnumber && (
|
{bodyshop.pbs_serialnumber && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.dms.disablecontactvehiclecreation")}
|
label={t("bodyshop.fields.dms.disablecontactvehiclecreation")}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import ResponsiveTable from "../responsive-table/responsive-table.component";
|
|||||||
|
|
||||||
export default function VehiclesListComponent({ loading, vehicles, total, refetch, basePath = "/manage" }) {
|
export default function VehiclesListComponent({ loading, vehicles, total, refetch, basePath = "/manage" }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const {
|
const { page, pageSize } = search;
|
||||||
page
|
|
||||||
//sortcolumn, sortorder,
|
|
||||||
} = search;
|
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
@@ -43,9 +44,7 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
|
|||||||
key: "description",
|
key: "description",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
<span>{`${record.v_model_yr || ""} ${record.v_color || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""} `}</span>
|
||||||
record.v_model_desc || ""
|
|
||||||
} ${record.v_color || ""}`}</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -62,10 +61,14 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
const nextPageSize = pagination?.pageSize || currentPageSize;
|
||||||
|
const pageSizeChanged = nextPageSize !== currentPageSize;
|
||||||
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
const updatedSearch = {
|
const updatedSearch = {
|
||||||
...search,
|
...search,
|
||||||
page: pagination.current,
|
pageSize: nextPageSize,
|
||||||
|
page: pageSizeChanged ? 1 : pagination.current,
|
||||||
sortcolumn: sorter.columnKey,
|
sortcolumn: sorter.columnKey,
|
||||||
sortorder: sorter.order
|
sortorder: sorter.order
|
||||||
};
|
};
|
||||||
@@ -106,7 +109,13 @@ export default function VehiclesListComponent({ loading, vehicles, total, refetc
|
|||||||
>
|
>
|
||||||
<ResponsiveTable
|
<ResponsiveTable
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ placement: "top", pageSize: pageLimit, current: parseInt(page || 1), total: total }}
|
pagination={{
|
||||||
|
placement: "top",
|
||||||
|
pageSize: currentPageSize,
|
||||||
|
current: currentPage,
|
||||||
|
showSizeChanger: true,
|
||||||
|
total: total
|
||||||
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
mobileColumnKeys={["v_vin", "description", "plate_no"]}
|
mobileColumnKeys={["v_vin", "description", "plate_no"]}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@@ -16,14 +16,18 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function VehiclesListContainer({ isPartsEntry }) {
|
export function VehiclesListContainer({ isPartsEntry }) {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const { page, sortcolumn, sortorder, search } = searchParams;
|
const { page, sortcolumn, sortorder, search, pageSize } = searchParams;
|
||||||
const basePath = getPartsBasePath(isPartsEntry);
|
const basePath = getPartsBasePath(isPartsEntry);
|
||||||
|
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_VEHICLES_PAGINATED, {
|
const { loading, error, data, refetch } = useQuery(QUERY_ALL_VEHICLES_PAGINATED, {
|
||||||
variables: {
|
variables: {
|
||||||
search: search || "",
|
search: search || "",
|
||||||
offset: page ? (page - 1) * pageLimit : 0,
|
offset: (currentPage - 1) * currentPageSize,
|
||||||
limit: pageLimit,
|
limit: currentPageSize,
|
||||||
order: [
|
order: [
|
||||||
{
|
{
|
||||||
[sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
[sortcolumn || "created_at"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getAnalytics, logEvent } from "@firebase/analytics";
|
//import { getAnalytics, logEvent } from "@firebase/analytics";
|
||||||
import { initializeApp } from "@firebase/app";
|
import { initializeApp } from "@firebase/app";
|
||||||
import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
|
import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
|
||||||
import { getFirestore } from "@firebase/firestore";
|
import { getFirestore } from "@firebase/firestore";
|
||||||
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||||
import { store } from "../redux/store";
|
//import { store } from "../redux/store";
|
||||||
//import * as amplitude from '@amplitude/analytics-browser';
|
//import * as amplitude from '@amplitude/analytics-browser';
|
||||||
// import posthog from 'posthog-js'
|
// import posthog from 'posthog-js'
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ initializeApp(config);
|
|||||||
|
|
||||||
export const auth = getAuth();
|
export const auth = getAuth();
|
||||||
export const firestore = getFirestore();
|
export const firestore = getFirestore();
|
||||||
export const analytics = getAnalytics();
|
//export const analytics = getAnalytics();
|
||||||
|
|
||||||
//export default firebase;
|
//export default firebase;
|
||||||
export const getCurrentUser = () => {
|
export const getCurrentUser = () => {
|
||||||
@@ -72,34 +72,36 @@ onMessage(messaging, (payload) => {
|
|||||||
// ...
|
// ...
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
// eslint-disable-next-line no-unused-vars
|
||||||
try {
|
export const logImEXEvent = (eventName, additionalParams, _stateProp = null) => {
|
||||||
const state = stateProp || store.getState();
|
// Disabled as a part of IO-3712.
|
||||||
|
// try {
|
||||||
|
// const state = stateProp || store.getState();
|
||||||
|
|
||||||
const eventParams = {
|
// const eventParams = {
|
||||||
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
|
// shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
|
||||||
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
// user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||||
partsManagementOnly: state?.user?.partsManagementOnly,
|
// partsManagementOnly: state?.user?.partsManagementOnly,
|
||||||
...additionalParams
|
// ...additionalParams
|
||||||
};
|
// };
|
||||||
// axios.post("/ioevent", {
|
// // axios.post("/ioevent", {
|
||||||
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
// // useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||||
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
|
// // bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
|
||||||
// operationName: eventName,
|
// // operationName: eventName,
|
||||||
// variables: additionalParams,
|
// // variables: additionalParams,
|
||||||
// dbevent: false,
|
// // dbevent: false,
|
||||||
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
// // env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||||
// });
|
// // });
|
||||||
// console.log(
|
// // console.log(
|
||||||
// "%c[Analytics]",
|
// // "%c[Analytics]",
|
||||||
// "background-color: green ;font-weight:bold;",
|
// // "background-color: green ;font-weight:bold;",
|
||||||
// eventName,
|
// // eventName,
|
||||||
// eventParams
|
// // eventParams
|
||||||
// );
|
// // );
|
||||||
logEvent(analytics, eventName, eventParams);
|
// logEvent(analytics, eventName, eventParams);
|
||||||
//amplitude.track(eventName, eventParams);
|
// //amplitude.track(eventName, eventParams);
|
||||||
//posthog.capture(eventName, eventParams);
|
// //posthog.capture(eventName, eventParams);
|
||||||
} finally {
|
// } finally {
|
||||||
//If it fails, just keep going.
|
// //If it fails, just keep going.
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ export const QUERY_AUDIT_TRAIL = gql`
|
|||||||
useremail
|
useremail
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
esignature_documents(where: {jobid: {_eq: $jobid}}) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
jobid
|
||||||
|
external_document_id
|
||||||
|
subject
|
||||||
|
message
|
||||||
|
title
|
||||||
|
status
|
||||||
|
recipients
|
||||||
|
completed_at
|
||||||
|
opened
|
||||||
|
completed
|
||||||
|
rejected
|
||||||
|
completed_at
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
phone
|
phone
|
||||||
federal_tax_id
|
federal_tax_id
|
||||||
id
|
id
|
||||||
|
documenso_api_key
|
||||||
insurance_vendor_id
|
insurance_vendor_id
|
||||||
logo_img_path
|
logo_img_path
|
||||||
md_ro_statuses
|
md_ro_statuses
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import reportWebVitals from "./reportWebVitals";
|
|||||||
import "./translations/i18n";
|
import "./translations/i18n";
|
||||||
import "./utils/CleanAxios";
|
import "./utils/CleanAxios";
|
||||||
// import * as amplitude from "@amplitude/analytics-browser";
|
// import * as amplitude from "@amplitude/analytics-browser";
|
||||||
import { PostHogProvider } from "posthog-js/react";
|
//import { PostHogProvider } from "posthog-js/react";
|
||||||
import posthog from "posthog-js";
|
//import posthog from "posthog-js";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
|
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
@@ -44,11 +44,11 @@ Dinero.globalRoundingMode = "HALF_EVEN";
|
|||||||
// // }
|
// // }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
// posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||||
autocapture: false,
|
// autocapture: false,
|
||||||
capture_exceptions: true,
|
// capture_exceptions: true,
|
||||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST
|
// api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST
|
||||||
});
|
// });
|
||||||
|
|
||||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||||
|
|
||||||
@@ -70,9 +70,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
||||||
<PostHogProvider client={posthog}>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</PostHogProvider>
|
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,16 +18,20 @@ const mapStateToProps = createStructuredSelector({});
|
|||||||
|
|
||||||
export function ExportLogsPageComponent() {
|
export function ExportLogsPageComponent() {
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const { page, sortcolumn, sortorder, search } = searchParams;
|
const { page, sortcolumn, sortorder, search, pageSize } = searchParams;
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
|
||||||
|
const currentPage = Number.parseInt(page || "1", 10);
|
||||||
|
const parsedPageSize = Number.parseInt(pageSize || String(pageLimit), 10);
|
||||||
|
const currentPageSize = Number.isNaN(parsedPageSize) ? pageLimit : parsedPageSize;
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_EXPORT_LOG_PAGINATED, {
|
const { loading, error, data, refetch } = useQuery(QUERY_EXPORT_LOG_PAGINATED, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
search: search || "",
|
search: search || "",
|
||||||
offset: page ? (page - 1) * pageLimit : 0,
|
offset: (currentPage - 1) * currentPageSize,
|
||||||
limit: pageLimit,
|
limit: currentPageSize,
|
||||||
order: [
|
order: [
|
||||||
{
|
{
|
||||||
...(sortcolumn === "ro_number"
|
...(sortcolumn === "ro_number"
|
||||||
@@ -61,7 +65,11 @@ export function ExportLogsPageComponent() {
|
|||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
searchParams.page = pagination.current;
|
const nextPageSize = pagination?.pageSize || currentPageSize;
|
||||||
|
const pageSizeChanged = nextPageSize !== currentPageSize;
|
||||||
|
|
||||||
|
searchParams.pageSize = nextPageSize;
|
||||||
|
searchParams.page = pageSizeChanged ? 1 : pagination.current;
|
||||||
searchParams.sortcolumn = sorter.columnKey;
|
searchParams.sortcolumn = sorter.columnKey;
|
||||||
searchParams.sortorder = sorter.order;
|
searchParams.sortorder = sorter.order;
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
@@ -191,8 +199,9 @@ export function ExportLogsPageComponent() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
placement: "top",
|
placement: "top",
|
||||||
pageSize: pageLimit,
|
pageSize: currentPageSize,
|
||||||
current: parseInt(page || 1, 10),
|
current: currentPage,
|
||||||
|
showSizeChanger: true,
|
||||||
total: data && data.search_exportlog_aggregate.aggregate.count
|
total: data && data.search_exportlog_aggregate.aggregate.count
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Col, Form, Input, Modal, Result, Row, Select, Space, Switch, Typography } from "antd";
|
import { Button, Card, Col, Form, Input, Modal, Result, Row, Select, Space, Switch, Typography } from "antd";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@@ -23,9 +23,8 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
|
|||||||
import NotFound from "../../components/not-found/not-found.component";
|
import NotFound from "../../components/not-found/not-found.component";
|
||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal";
|
import RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal";
|
||||||
import { GET_JOB_BY_PK, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
import { CONVERT_JOB_TO_RO, GET_JOB_BY_PK } from "../../graphql/jobs.queries";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket";
|
import { useSocket } from "../../contexts/SocketIO/useSocket";
|
||||||
@@ -302,7 +301,11 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select>
|
<Select
|
||||||
|
showSearch={{
|
||||||
|
optionFilterProp: "children"
|
||||||
|
}}
|
||||||
|
>
|
||||||
{bodyshop?.md_referral_sources?.map((s) => (
|
{bodyshop?.md_referral_sources?.map((s) => (
|
||||||
<Select.Option key={s} value={s}>
|
<Select.Option key={s} value={s}>
|
||||||
{s}
|
{s}
|
||||||
@@ -379,7 +382,13 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Space wrap style={{ marginTop: 16 }}>
|
<Space wrap style={{ marginTop: 16 }}>
|
||||||
<Button disabled={submitDisabled()} type="primary" danger onClick={() => form.submit()} loading={convertLoading}>
|
<Button
|
||||||
|
disabled={submitDisabled()}
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
onClick={() => form.submit()}
|
||||||
|
loading={convertLoading}
|
||||||
|
>
|
||||||
{t("jobs.actions.convert")}
|
{t("jobs.actions.convert")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setShowConvertModal(false)}>{t("general.actions.close")}</Button>
|
<Button onClick={() => setShowConvertModal(false)}>{t("general.actions.close")}</Button>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import EsignatureCustomDocument from "../../components/esignature-custom-document/esignature-custom-document.component.jsx";
|
||||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||||
@@ -56,6 +57,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -104,6 +106,7 @@ export function JobsDetailPage({
|
|||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const { scenarioNotificationsOn } = useSocket();
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//form.setFieldsValue(transormJobToForm(job));
|
//form.setFieldsValue(transormJobToForm(job));
|
||||||
@@ -285,6 +288,7 @@ export function JobsDetailPage({
|
|||||||
>
|
>
|
||||||
{t("general.labels.refresh")}
|
{t("general.labels.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{esignatureEnabled && <EsignatureCustomDocument jobId={job.id} />}
|
||||||
<JobsChangeStatus job={job} />
|
<JobsChangeStatus job={job} />
|
||||||
<JobSyncButton job={job} />
|
<JobSyncButton job={job} />
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ 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";
|
||||||
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
|
||||||
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 +70,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 +114,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({
|
||||||
@@ -123,6 +129,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, currentUser }) {
|
export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, currentUser }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
const [chatVisible] = useState(false);
|
const [chatVisible] = useState(false);
|
||||||
const didMount = useRef(false);
|
const didMount = useRef(false);
|
||||||
|
|
||||||
@@ -178,6 +185,7 @@ export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, cu
|
|||||||
<TaskUpsertModalContainer />
|
<TaskUpsertModalContainer />
|
||||||
<BreadCrumbs />
|
<BreadCrumbs />
|
||||||
<BillEnterModalContainer />
|
<BillEnterModalContainer />
|
||||||
|
{esignatureEnabled && <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,5 +1,5 @@
|
|||||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||||
import { setUserId, setUserProperties } from "@firebase/analytics";
|
//import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
confirmPasswordReset,
|
confirmPasswordReset,
|
||||||
@@ -9,14 +9,13 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import * as Sentry from "@sentry/react";
|
// import * as Sentry from "@sentry/react";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
//import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import {
|
import {
|
||||||
analytics,
|
|
||||||
auth,
|
auth,
|
||||||
firestore,
|
firestore,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
@@ -49,7 +48,7 @@ import {
|
|||||||
validatePasswordResetSuccess
|
validatePasswordResetSuccess
|
||||||
} from "./user.actions";
|
} from "./user.actions";
|
||||||
import UserActionTypes from "./user.types";
|
import UserActionTypes from "./user.types";
|
||||||
import posthog from "posthog-js";
|
//import posthog from "posthog-js";
|
||||||
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
|
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
|
||||||
|
|
||||||
const fpPromise = FingerprintJS.load();
|
const fpPromise = FingerprintJS.load();
|
||||||
@@ -91,9 +90,9 @@ export function* isUserAuthenticated() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogRocket.identify(user.email);
|
//LogRocket.identify(user.email);
|
||||||
//amplitude.setUserId(user.email);
|
//amplitude.setUserId(user.email);
|
||||||
posthog.identify(user.email);
|
//posthog.identify(user.email);
|
||||||
|
|
||||||
const eulaQuery = yield client.query({
|
const eulaQuery = yield client.query({
|
||||||
query: QUERY_EULA,
|
query: QUERY_EULA,
|
||||||
@@ -234,7 +233,7 @@ export function* onSignInSuccess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function* signInSuccessSaga({ payload }) {
|
export function* signInSuccessSaga({ payload }) {
|
||||||
LogRocket.identify(payload.email);
|
//LogRocket.identify(payload.email);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.$crisp?.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
window.$crisp?.push(["set", "user:nickname", [payload.displayName || payload.email]]);
|
||||||
@@ -279,17 +278,17 @@ export function* signInSuccessSaga({ payload }) {
|
|||||||
console.log("Error updating Crisp settings.", error);
|
console.log("Error updating Crisp settings.", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
Sentry.setUser({
|
// Sentry.setUser({
|
||||||
email: payload.email,
|
// email: payload.email,
|
||||||
username: payload.displayName || payload.email
|
// username: payload.displayName || payload.email
|
||||||
});
|
// });
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.log("Error setting Sentry user.", error);
|
// console.log("Error setting Sentry user.", error);
|
||||||
}
|
// }
|
||||||
|
|
||||||
setUserId(analytics, payload.email);
|
// setUserId(analytics, payload.email);
|
||||||
setUserProperties(analytics, payload);
|
// setUserProperties(analytics, payload);
|
||||||
yield;
|
yield;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -323,14 +323,14 @@
|
|||||||
"addtemplate": "Add Template",
|
"addtemplate": "Add Template",
|
||||||
"newlaborrate": "New Labor Rate",
|
"newlaborrate": "New Labor Rate",
|
||||||
"newsalestaxcode": "New Sales Tax Code",
|
"newsalestaxcode": "New Sales Tax Code",
|
||||||
"save_shop_information": "Save Shop Information",
|
|
||||||
"newstatus": "Add Status",
|
"newstatus": "Add Status",
|
||||||
|
"save_shop_information": "Save Shop Information",
|
||||||
"testrender": "Test Render"
|
"testrender": "Test Render"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"creatingdefaultview": "Error creating default view.",
|
"creatingdefaultview": "Error creating default view.",
|
||||||
"duplicate_job_status": "Duplicate job status. Each job status must be unique.",
|
|
||||||
"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",
|
||||||
|
"duplicate_job_status": "Duplicate job status. Each job status 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%."
|
"task_preset_allocation_exceeded": "{{laborType}} task preset total is {{total}}% and cannot exceed 100%."
|
||||||
@@ -370,6 +370,7 @@
|
|||||||
"cashierid": "Cashier ID",
|
"cashierid": "Cashier ID",
|
||||||
"default_journal": "Default Journal",
|
"default_journal": "Default Journal",
|
||||||
"disablebillwip": "Disable bill WIP for A/P Posting",
|
"disablebillwip": "Disable bill WIP for A/P Posting",
|
||||||
|
"disablecontact": "Disable Contact Updates/Creation",
|
||||||
"disablecontactvehiclecreation": "Disable Contact & Vehicle Updates/Creation",
|
"disablecontactvehiclecreation": "Disable Contact & Vehicle Updates/Creation",
|
||||||
"dms_acctnumber": "DMS Account #",
|
"dms_acctnumber": "DMS Account #",
|
||||||
"dms_control_override": "Static Control # Override",
|
"dms_control_override": "Static Control # Override",
|
||||||
@@ -427,35 +428,6 @@
|
|||||||
"logo_img_path": "Shop Logo",
|
"logo_img_path": "Shop Logo",
|
||||||
"logo_img_path_height": "Logo Image Height",
|
"logo_img_path_height": "Logo Image Height",
|
||||||
"logo_img_path_width": "Logo Image Width",
|
"logo_img_path_width": "Logo Image Width",
|
||||||
"scoreboard_setup": {
|
|
||||||
"daily_body_target": "Daily Body Target",
|
|
||||||
"daily_paint_target": "Daily Paint Target",
|
|
||||||
"ignore_blocked_days": "Ignore Blocked Days",
|
|
||||||
"last_number_working_days": "Last Number of Working Days",
|
|
||||||
"production_target_hours": "Production Target Hours"
|
|
||||||
},
|
|
||||||
"system_settings": {
|
|
||||||
"auto_email": {
|
|
||||||
"attach_pdf_to_email": "Attach PDF to Sent Emails?",
|
|
||||||
"from_emails": "Additional From Emails",
|
|
||||||
"parts_order_cc": "Parts Orders CC",
|
|
||||||
"parts_return_slip_cc": "Parts Returns CC"
|
|
||||||
},
|
|
||||||
"job_costing": {
|
|
||||||
"paint_hour_split": "Paint Hour Split",
|
|
||||||
"paint_materials_hourly_cost_rate": "Paint Materials Hourly Cost Rate",
|
|
||||||
"prep_hour_split": "Prep Hour Split",
|
|
||||||
"shop_materials_hourly_cost_rate": "Shop Materials Hourly Cost Rate",
|
|
||||||
"target_touch_time": "Target Touch Time",
|
|
||||||
"use_paint_scale_data": "Use Paint Scale Data"
|
|
||||||
},
|
|
||||||
"local_media_server": {
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"http_path": "HTTP Path",
|
|
||||||
"network_path": "Network Path",
|
|
||||||
"token": "Token"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"md_categories": "Categories",
|
"md_categories": "Categories",
|
||||||
"md_ccc_rates": "Courtesy Car Contract Rate Presets",
|
"md_ccc_rates": "Courtesy Car Contract Rate Presets",
|
||||||
"md_classes": "Classes",
|
"md_classes": "Classes",
|
||||||
@@ -463,6 +435,7 @@
|
|||||||
"md_email_cc": "Auto Email CC: $t(parts_orders.labels.{{template}})",
|
"md_email_cc": "Auto Email CC: $t(parts_orders.labels.{{template}})",
|
||||||
"md_from_emails": "Additional From Emails",
|
"md_from_emails": "Additional From Emails",
|
||||||
"md_functionality_toggles": {
|
"md_functionality_toggles": {
|
||||||
|
"enhanced_early_ros": "Enable Enhance Early ROs",
|
||||||
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
|
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
|
||||||
},
|
},
|
||||||
"md_hour_split": {
|
"md_hour_split": {
|
||||||
@@ -711,6 +684,13 @@
|
|||||||
},
|
},
|
||||||
"schedule_end_time": "Schedule Ending Time",
|
"schedule_end_time": "Schedule Ending Time",
|
||||||
"schedule_start_time": "Schedule Starting Time",
|
"schedule_start_time": "Schedule Starting Time",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "Daily Body Target",
|
||||||
|
"daily_paint_target": "Daily Paint Target",
|
||||||
|
"ignore_blocked_days": "Ignore Blocked Days",
|
||||||
|
"last_number_working_days": "Last Number of Working Days",
|
||||||
|
"production_target_hours": "Production Target Hours"
|
||||||
|
},
|
||||||
"shopname": "Shop Name",
|
"shopname": "Shop Name",
|
||||||
"speedprint": {
|
"speedprint": {
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
@@ -757,6 +737,28 @@
|
|||||||
"production_statuses": "Production Statuses",
|
"production_statuses": "Production Statuses",
|
||||||
"ready_statuses": "Ready Statuses"
|
"ready_statuses": "Ready Statuses"
|
||||||
},
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "Attach PDF to Sent Emails?",
|
||||||
|
"from_emails": "Additional From Emails",
|
||||||
|
"parts_order_cc": "Parts Orders CC",
|
||||||
|
"parts_return_slip_cc": "Parts Returns CC"
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "Paint Hour Split",
|
||||||
|
"paint_materials_hourly_cost_rate": "Paint Materials Hourly Cost Rate",
|
||||||
|
"prep_hour_split": "Prep Hour Split",
|
||||||
|
"shop_materials_hourly_cost_rate": "Shop Materials Hourly Cost Rate",
|
||||||
|
"target_touch_time": "Target Touch Time",
|
||||||
|
"use_paint_scale_data": "Use Paint Scale Data"
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"http_path": "HTTP Path",
|
||||||
|
"network_path": "Network Path",
|
||||||
|
"token": "Token"
|
||||||
|
}
|
||||||
|
},
|
||||||
"target_touchtime": "Target Touch Time",
|
"target_touchtime": "Target Touch Time",
|
||||||
"timezone": "Timezone",
|
"timezone": "Timezone",
|
||||||
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
|
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
|
||||||
@@ -776,6 +778,7 @@
|
|||||||
"alljobstatuses": "All Job Statuses",
|
"alljobstatuses": "All Job Statuses",
|
||||||
"allopenjobstatuses": "All Open Job Statuses",
|
"allopenjobstatuses": "All Open Job Statuses",
|
||||||
"apptcolors": "Appointment Colors",
|
"apptcolors": "Appointment Colors",
|
||||||
|
"autoemail": "Auto Email",
|
||||||
"businessinformation": "Business Information",
|
"businessinformation": "Business Information",
|
||||||
"checklists": "Checklists",
|
"checklists": "Checklists",
|
||||||
"consent_settings": "Phone Number Opt-Out List",
|
"consent_settings": "Phone Number Opt-Out List",
|
||||||
@@ -783,7 +786,6 @@
|
|||||||
"customtemplates": "Custom Templates",
|
"customtemplates": "Custom Templates",
|
||||||
"defaultcostsmapping": "Default Costs Mapping",
|
"defaultcostsmapping": "Default Costs Mapping",
|
||||||
"defaultprofitsmapping": "Default Profits Mapping",
|
"defaultprofitsmapping": "Default Profits Mapping",
|
||||||
"dms_setup": "DMS Setup",
|
|
||||||
"deliverchecklist": "Delivery Checklist",
|
"deliverchecklist": "Delivery Checklist",
|
||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
@@ -798,10 +800,11 @@
|
|||||||
"rr_dealerid": "Reynolds Store Number",
|
"rr_dealerid": "Reynolds Store Number",
|
||||||
"title": "DMS"
|
"title": "DMS"
|
||||||
},
|
},
|
||||||
|
"dms_setup": "DMS Setup",
|
||||||
"emaillater": "Email Later",
|
"emaillater": "Email Later",
|
||||||
"employee_teams": "Employee Teams",
|
|
||||||
"employee_options": "Employee Options",
|
"employee_options": "Employee Options",
|
||||||
"employee_rates": "Employee Rates",
|
"employee_rates": "Employee Rates",
|
||||||
|
"employee_teams": "Employee Teams",
|
||||||
"employee_vacation": "Employee Vacation",
|
"employee_vacation": "Employee Vacation",
|
||||||
"employees": "Employees",
|
"employees": "Employees",
|
||||||
"estimators": "Estimators",
|
"estimators": "Estimators",
|
||||||
@@ -812,21 +815,22 @@
|
|||||||
"intakechecklist": "Intake Checklist",
|
"intakechecklist": "Intake Checklist",
|
||||||
"intellipay_cash_discount": "Please ensure that cash discounting has been enabled on your merchant account. Reach out to IntelliPay Support if you need assistance. ",
|
"intellipay_cash_discount": "Please ensure that cash discounting has been enabled on your merchant account. Reach out to IntelliPay Support if you need assistance. ",
|
||||||
"job_status_options": "Job Status Options",
|
"job_status_options": "Job Status Options",
|
||||||
|
"jobcosting": "Job Costing",
|
||||||
"jobstatuses": "Job Statuses",
|
"jobstatuses": "Job Statuses",
|
||||||
|
"jump_to_section": "Jump to section",
|
||||||
"laborrates": "Labor Rates",
|
"laborrates": "Labor Rates",
|
||||||
"licensing": "Licensing",
|
"licensing": "Licensing",
|
||||||
|
"localmediaserver": "Local Media Server",
|
||||||
"md_parts_scan": "Parts Scan Rules",
|
"md_parts_scan": "Parts Scan Rules",
|
||||||
"md_ro_guard": "RO Guard",
|
"md_ro_guard": "RO Guard",
|
||||||
"md_ro_guard_options": "RO Guard Options",
|
"md_ro_guard_options": "RO Guard Options",
|
||||||
"md_tasks_presets": "Tasks Presets",
|
"md_tasks_presets": "Tasks Presets",
|
||||||
"task_preset_options": "Task Preset Options",
|
|
||||||
"md_to_emails": "Preset To Emails",
|
"md_to_emails": "Preset To Emails",
|
||||||
"md_to_emails_emails": "Emails",
|
"md_to_emails_emails": "Emails",
|
||||||
"messagingpresets": "Messaging Presets",
|
"messagingpresets": "Messaging Presets",
|
||||||
"notification_options": "Notification Options",
|
|
||||||
"notemplatesavailable": "No templates available to add.",
|
"notemplatesavailable": "No templates available to add.",
|
||||||
"notespresets": "Notes Presets",
|
"notespresets": "Notes Presets",
|
||||||
"jump_to_section": "Jump to section",
|
"notification_options": "Notification Options",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": "Notifications"
|
"followers": "Notifications"
|
||||||
},
|
},
|
||||||
@@ -863,9 +867,6 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": "RO Guard"
|
"title": "RO Guard"
|
||||||
},
|
},
|
||||||
"autoemail": "Auto Email",
|
|
||||||
"jobcosting": "Job Costing",
|
|
||||||
"localmediaserver": "Local Media Server",
|
|
||||||
"romepay": "Rome Pay",
|
"romepay": "Rome Pay",
|
||||||
"scheduling": "SMART Scheduling",
|
"scheduling": "SMART Scheduling",
|
||||||
"scoreboardsetup": "Scoreboard Setup",
|
"scoreboardsetup": "Scoreboard Setup",
|
||||||
@@ -877,6 +878,7 @@
|
|||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
|
"task_preset_options": "Task Preset Options",
|
||||||
"workingdays": "Working Days"
|
"workingdays": "Working Days"
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
@@ -1221,6 +1223,7 @@
|
|||||||
"confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.",
|
"confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.",
|
||||||
"doctype": "Document Type",
|
"doctype": "Document Type",
|
||||||
"dragtoupload": "Click or drag files to this area to upload",
|
"dragtoupload": "Click or drag files to this area to upload",
|
||||||
|
"greyscale": "Greyscale",
|
||||||
"newjobid": "Assign to Job",
|
"newjobid": "Assign to Job",
|
||||||
"openinexplorer": "Open in Explorer",
|
"openinexplorer": "Open in Explorer",
|
||||||
"optimizedimage": "The below image is optimized. Click on the picture below to open in a new window and view it full size, or open it in explorer.",
|
"optimizedimage": "The below image is optimized. Click on the picture below to open in a new window and view it full size, or open it in explorer.",
|
||||||
@@ -1351,6 +1354,31 @@
|
|||||||
"unique_employee_number": "You must enter a unique employee number."
|
"unique_employee_number": "You must enter a unique employee number."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"delete": "Delete",
|
||||||
|
"distribute": "Distribute",
|
||||||
|
"redistribute": "Redistribute",
|
||||||
|
"upload_document": "Upload Document for E-Sign",
|
||||||
|
"view": "View"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"no_token": "Error connecting to signing server. No authorization token was provided.",
|
||||||
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"completed": "Completed?",
|
||||||
|
"completed_at": "Completed At",
|
||||||
|
"created_at": "Created At",
|
||||||
|
"external_document_id": "Ex. Document ID",
|
||||||
|
"opened": "Opened?",
|
||||||
|
"rejected": "Rejected?",
|
||||||
|
"status": "Status",
|
||||||
|
"title": "Title",
|
||||||
|
"updated_at": "Updated At"
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
@@ -1466,8 +1494,8 @@
|
|||||||
"beta": "BETA",
|
"beta": "BETA",
|
||||||
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
"cancel": "Are you sure you want to cancel? Your changes will not be saved.",
|
||||||
"changelog": "Change Log",
|
"changelog": "Change Log",
|
||||||
"click_to_begin": "Click {{action}} to begin",
|
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"click_to_begin": "Click {{action}} to begin",
|
||||||
"confirmpassword": "Confirm Password",
|
"confirmpassword": "Confirm Password",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
"date": "Select Date",
|
"date": "Select Date",
|
||||||
@@ -1785,9 +1813,9 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addpayer": "Add Payer",
|
|
||||||
"addDocuments": "Add Job Documents",
|
"addDocuments": "Add Job Documents",
|
||||||
"addNote": "Add Note",
|
"addNote": "Add Note",
|
||||||
|
"addpayer": "Add Payer",
|
||||||
"addtopartsqueue": "Add to Parts Queue",
|
"addtopartsqueue": "Add to Parts Queue",
|
||||||
"addtoproduction": "Add to Production",
|
"addtoproduction": "Add to Production",
|
||||||
"addtoscoreboard": "Add to Scoreboard",
|
"addtoscoreboard": "Add to Scoreboard",
|
||||||
@@ -1964,6 +1992,7 @@
|
|||||||
"ded_status": "Deductible Status",
|
"ded_status": "Deductible Status",
|
||||||
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
"depreciation_taxes": "Betterment/Depreciation/Taxes",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"IsARCustomer": "AR Customer?",
|
||||||
"address": "Customer Address",
|
"address": "Customer Address",
|
||||||
"advisor": "Advisor #",
|
"advisor": "Advisor #",
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
@@ -2314,6 +2343,8 @@
|
|||||||
"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_imex": "ImEX Sign",
|
||||||
|
"esignature_rome": "Rome Sign",
|
||||||
"estimatelines": "Estimate Lines",
|
"estimatelines": "Estimate Lines",
|
||||||
"estimator": "Estimator",
|
"estimator": "Estimator",
|
||||||
"existing_jobs": "Existing Jobs",
|
"existing_jobs": "Existing Jobs",
|
||||||
@@ -2753,6 +2784,9 @@
|
|||||||
"alternate-transport-changed": "Alternate Transport Changed",
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
"bill-posted": "Bill Posted",
|
"bill-posted": "Bill Posted",
|
||||||
"critical-parts-status-changed": "Critical Parts Status Changed",
|
"critical-parts-status-changed": "Critical Parts Status Changed",
|
||||||
|
"esign-document-completed": "E-Sign Document Completed",
|
||||||
|
"esign-document-opened": "E-Sign Document Opened",
|
||||||
|
"esign-document-upload-failed": "E-Sign Document Upload Failed",
|
||||||
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
"intake-delivery-checklist-completed": "Intake or Delivery Checklist Completed",
|
||||||
"job-added-to-production": "Job Added to Production",
|
"job-added-to-production": "Job Added to Production",
|
||||||
"job-assigned-to-me": "Job Assigned to Me",
|
"job-assigned-to-me": "Job Assigned to Me",
|
||||||
@@ -3265,6 +3299,7 @@
|
|||||||
"information": "Information",
|
"information": "Information",
|
||||||
"layout": "Layout",
|
"layout": "Layout",
|
||||||
"statistics": {
|
"statistics": {
|
||||||
|
"exclude_suspended": "Exclude Suspended Jobs",
|
||||||
"jobs_in_production": "Jobs in Production",
|
"jobs_in_production": "Jobs in Production",
|
||||||
"tasks_in_production": "Tasks in Production",
|
"tasks_in_production": "Tasks in Production",
|
||||||
"tasks_in_view": "Tasks in View",
|
"tasks_in_view": "Tasks in View",
|
||||||
@@ -3756,11 +3791,11 @@
|
|||||||
"jobhours": "Job Related Time Tickets Summary",
|
"jobhours": "Job Related Time Tickets Summary",
|
||||||
"lunch": "Lunch",
|
"lunch": "Lunch",
|
||||||
"new": "New Time Ticket",
|
"new": "New Time Ticket",
|
||||||
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
|
|
||||||
"payout_methods": {
|
"payout_methods": {
|
||||||
"commission": "Commission",
|
"commission": "Commission",
|
||||||
"hourly": "Hourly"
|
"hourly": "Hourly"
|
||||||
},
|
},
|
||||||
|
"payrollclaimedtasks": "These time tickets will be automatically entered to the system as a part of claiming this task. These numbers are calculated using the jobs assigned lines. If lines are unassigned, they will be excluded from created tickets.",
|
||||||
"pmbreak": "PM Break",
|
"pmbreak": "PM Break",
|
||||||
"pmshift": "PM Shift",
|
"pmshift": "PM Shift",
|
||||||
"shift": "Shift",
|
"shift": "Shift",
|
||||||
|
|||||||
@@ -120,8 +120,9 @@
|
|||||||
"appointmentinsert": "",
|
"appointmentinsert": "",
|
||||||
"assignedlinehours": "",
|
"assignedlinehours": "",
|
||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
"billposted": "",
|
|
||||||
"billmarkforreexport": "",
|
"billmarkforreexport": "",
|
||||||
|
"billposted": "",
|
||||||
|
"billupdated": "",
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
@@ -136,6 +137,9 @@
|
|||||||
"jobintake": "",
|
"jobintake": "",
|
||||||
"jobinvoiced": "",
|
"jobinvoiced": "",
|
||||||
"jobioucreated": "",
|
"jobioucreated": "",
|
||||||
|
"joblineupdate": "",
|
||||||
|
"jobmanualcreate": "",
|
||||||
|
"jobmanuallineinsert": "",
|
||||||
"jobmodifylbradj": "",
|
"jobmodifylbradj": "",
|
||||||
"jobnoteadded": "",
|
"jobnoteadded": "",
|
||||||
"jobnotedeleted": "",
|
"jobnotedeleted": "",
|
||||||
@@ -151,7 +155,9 @@
|
|||||||
"tasks_deleted": "",
|
"tasks_deleted": "",
|
||||||
"tasks_uncompleted": "",
|
"tasks_uncompleted": "",
|
||||||
"tasks_undeleted": "",
|
"tasks_undeleted": "",
|
||||||
"tasks_updated": ""
|
"tasks_updated": "",
|
||||||
|
"timeticketcreated": "",
|
||||||
|
"timeticketupdated": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
@@ -317,14 +323,14 @@
|
|||||||
"addtemplate": "",
|
"addtemplate": "",
|
||||||
"newlaborrate": "",
|
"newlaborrate": "",
|
||||||
"newsalestaxcode": "",
|
"newsalestaxcode": "",
|
||||||
"save_shop_information": "",
|
|
||||||
"newstatus": "",
|
"newstatus": "",
|
||||||
|
"save_shop_information": "",
|
||||||
"testrender": ""
|
"testrender": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
"duplicate_job_status": "",
|
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
|
"duplicate_job_status": "",
|
||||||
"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": ""
|
"task_preset_allocation_exceeded": ""
|
||||||
@@ -364,6 +370,7 @@
|
|||||||
"cashierid": "",
|
"cashierid": "",
|
||||||
"default_journal": "",
|
"default_journal": "",
|
||||||
"disablebillwip": "",
|
"disablebillwip": "",
|
||||||
|
"disablecontact": "",
|
||||||
"disablecontactvehiclecreation": "",
|
"disablecontactvehiclecreation": "",
|
||||||
"dms_acctnumber": "",
|
"dms_acctnumber": "",
|
||||||
"dms_control_override": "",
|
"dms_control_override": "",
|
||||||
@@ -421,35 +428,6 @@
|
|||||||
"logo_img_path": "",
|
"logo_img_path": "",
|
||||||
"logo_img_path_height": "",
|
"logo_img_path_height": "",
|
||||||
"logo_img_path_width": "",
|
"logo_img_path_width": "",
|
||||||
"scoreboard_setup": {
|
|
||||||
"daily_body_target": "",
|
|
||||||
"daily_paint_target": "",
|
|
||||||
"ignore_blocked_days": "",
|
|
||||||
"last_number_working_days": "",
|
|
||||||
"production_target_hours": ""
|
|
||||||
},
|
|
||||||
"system_settings": {
|
|
||||||
"auto_email": {
|
|
||||||
"attach_pdf_to_email": "",
|
|
||||||
"from_emails": "",
|
|
||||||
"parts_order_cc": "",
|
|
||||||
"parts_return_slip_cc": ""
|
|
||||||
},
|
|
||||||
"job_costing": {
|
|
||||||
"paint_hour_split": "",
|
|
||||||
"paint_materials_hourly_cost_rate": "",
|
|
||||||
"prep_hour_split": "",
|
|
||||||
"shop_materials_hourly_cost_rate": "",
|
|
||||||
"target_touch_time": "",
|
|
||||||
"use_paint_scale_data": ""
|
|
||||||
},
|
|
||||||
"local_media_server": {
|
|
||||||
"enabled": "",
|
|
||||||
"http_path": "",
|
|
||||||
"network_path": "",
|
|
||||||
"token": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"md_categories": "",
|
"md_categories": "",
|
||||||
"md_ccc_rates": "",
|
"md_ccc_rates": "",
|
||||||
"md_classes": "",
|
"md_classes": "",
|
||||||
@@ -457,6 +435,7 @@
|
|||||||
"md_email_cc": "",
|
"md_email_cc": "",
|
||||||
"md_from_emails": "",
|
"md_from_emails": "",
|
||||||
"md_functionality_toggles": {
|
"md_functionality_toggles": {
|
||||||
|
"enhanced_early_ros": "",
|
||||||
"parts_queue_toggle": ""
|
"parts_queue_toggle": ""
|
||||||
},
|
},
|
||||||
"md_hour_split": {
|
"md_hour_split": {
|
||||||
@@ -705,6 +684,13 @@
|
|||||||
},
|
},
|
||||||
"schedule_end_time": "",
|
"schedule_end_time": "",
|
||||||
"schedule_start_time": "",
|
"schedule_start_time": "",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "",
|
||||||
|
"daily_paint_target": "",
|
||||||
|
"ignore_blocked_days": "",
|
||||||
|
"last_number_working_days": "",
|
||||||
|
"production_target_hours": ""
|
||||||
|
},
|
||||||
"shopname": "",
|
"shopname": "",
|
||||||
"speedprint": {
|
"speedprint": {
|
||||||
"id": "",
|
"id": "",
|
||||||
@@ -751,6 +737,28 @@
|
|||||||
"production_statuses": "",
|
"production_statuses": "",
|
||||||
"ready_statuses": ""
|
"ready_statuses": ""
|
||||||
},
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "",
|
||||||
|
"from_emails": "",
|
||||||
|
"parts_order_cc": "",
|
||||||
|
"parts_return_slip_cc": ""
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "",
|
||||||
|
"paint_materials_hourly_cost_rate": "",
|
||||||
|
"prep_hour_split": "",
|
||||||
|
"shop_materials_hourly_cost_rate": "",
|
||||||
|
"target_touch_time": "",
|
||||||
|
"use_paint_scale_data": ""
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "",
|
||||||
|
"http_path": "",
|
||||||
|
"network_path": "",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"target_touchtime": "",
|
"target_touchtime": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"tt_allow_post_to_invoiced": "",
|
"tt_allow_post_to_invoiced": "",
|
||||||
@@ -770,6 +778,7 @@
|
|||||||
"alljobstatuses": "",
|
"alljobstatuses": "",
|
||||||
"allopenjobstatuses": "",
|
"allopenjobstatuses": "",
|
||||||
"apptcolors": "",
|
"apptcolors": "",
|
||||||
|
"autoemail": "",
|
||||||
"businessinformation": "",
|
"businessinformation": "",
|
||||||
"checklists": "",
|
"checklists": "",
|
||||||
"consent_settings": "",
|
"consent_settings": "",
|
||||||
@@ -777,7 +786,6 @@
|
|||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
"defaultprofitsmapping": "",
|
"defaultprofitsmapping": "",
|
||||||
"dms_setup": "",
|
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
@@ -792,10 +800,11 @@
|
|||||||
"rr_dealerid": "",
|
"rr_dealerid": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"dms_setup": "",
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
"employee_teams": "",
|
|
||||||
"employee_options": "",
|
"employee_options": "",
|
||||||
"employee_rates": "",
|
"employee_rates": "",
|
||||||
|
"employee_teams": "",
|
||||||
"employee_vacation": "",
|
"employee_vacation": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
@@ -806,21 +815,22 @@
|
|||||||
"intakechecklist": "",
|
"intakechecklist": "",
|
||||||
"intellipay_cash_discount": "",
|
"intellipay_cash_discount": "",
|
||||||
"job_status_options": "",
|
"job_status_options": "",
|
||||||
|
"jobcosting": "",
|
||||||
"jobstatuses": "",
|
"jobstatuses": "",
|
||||||
|
"jump_to_section": "",
|
||||||
"laborrates": "",
|
"laborrates": "",
|
||||||
"licensing": "",
|
"licensing": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"md_parts_scan": "",
|
"md_parts_scan": "",
|
||||||
"md_ro_guard": "",
|
"md_ro_guard": "",
|
||||||
"md_ro_guard_options": "",
|
"md_ro_guard_options": "",
|
||||||
"md_tasks_presets": "",
|
"md_tasks_presets": "",
|
||||||
"task_preset_options": "",
|
|
||||||
"md_to_emails": "",
|
"md_to_emails": "",
|
||||||
"md_to_emails_emails": "",
|
"md_to_emails_emails": "",
|
||||||
"messagingpresets": "",
|
"messagingpresets": "",
|
||||||
"notification_options": "",
|
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
"jump_to_section": "",
|
"notification_options": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
@@ -857,9 +867,6 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
"autoemail": "",
|
|
||||||
"jobcosting": "",
|
|
||||||
"localmediaserver": "",
|
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
@@ -871,6 +878,7 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
|
"task_preset_options": "",
|
||||||
"workingdays": ""
|
"workingdays": ""
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
@@ -1215,6 +1223,7 @@
|
|||||||
"confirmdelete": "",
|
"confirmdelete": "",
|
||||||
"doctype": "",
|
"doctype": "",
|
||||||
"dragtoupload": "",
|
"dragtoupload": "",
|
||||||
|
"greyscale": "Escala de grises",
|
||||||
"newjobid": "",
|
"newjobid": "",
|
||||||
"openinexplorer": "",
|
"openinexplorer": "",
|
||||||
"optimizedimage": "",
|
"optimizedimage": "",
|
||||||
@@ -1345,6 +1354,31 @@
|
|||||||
"unique_employee_number": ""
|
"unique_employee_number": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"delete": "",
|
||||||
|
"distribute": "",
|
||||||
|
"redistribute": "",
|
||||||
|
"upload_document": "Upload Document for E-Sign",
|
||||||
|
"view": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"no_token": "",
|
||||||
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"completed": "",
|
||||||
|
"completed_at": "",
|
||||||
|
"created_at": "",
|
||||||
|
"external_document_id": "",
|
||||||
|
"opened": "",
|
||||||
|
"rejected": "",
|
||||||
|
"status": "",
|
||||||
|
"title": "",
|
||||||
|
"updated_at": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
@@ -1460,8 +1494,8 @@
|
|||||||
"beta": "",
|
"beta": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"changelog": "",
|
"changelog": "",
|
||||||
"click_to_begin": "",
|
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"click_to_begin": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
"date": "",
|
"date": "",
|
||||||
@@ -1779,9 +1813,9 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addpayer": "",
|
|
||||||
"addDocuments": "Agregar documentos de trabajo",
|
"addDocuments": "Agregar documentos de trabajo",
|
||||||
"addNote": "Añadir la nota",
|
"addNote": "Añadir la nota",
|
||||||
|
"addpayer": "",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"addtoscoreboard": "",
|
"addtoscoreboard": "",
|
||||||
@@ -1958,6 +1992,7 @@
|
|||||||
"ded_status": "Estado deducible",
|
"ded_status": "Estado deducible",
|
||||||
"depreciation_taxes": "Depreciación / Impuestos",
|
"depreciation_taxes": "Depreciación / Impuestos",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"IsARCustomer": "",
|
||||||
"address": "",
|
"address": "",
|
||||||
"advisor": "",
|
"advisor": "",
|
||||||
"amount": "",
|
"amount": "",
|
||||||
@@ -2308,6 +2343,8 @@
|
|||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"emailaudit": "",
|
"emailaudit": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"esignature_imex": "",
|
||||||
|
"esignature_rome": "",
|
||||||
"estimatelines": "",
|
"estimatelines": "",
|
||||||
"estimator": "",
|
"estimator": "",
|
||||||
"existing_jobs": "Empleos existentes",
|
"existing_jobs": "Empleos existentes",
|
||||||
@@ -2747,6 +2784,9 @@
|
|||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
"bill-posted": "",
|
"bill-posted": "",
|
||||||
"critical-parts-status-changed": "",
|
"critical-parts-status-changed": "",
|
||||||
|
"esign-document-completed": "E-Sign Document Completed",
|
||||||
|
"esign-document-opened": "E-Sign Document Opened",
|
||||||
|
"esign-document-upload-failed": "E-Sign Document Upload Failed",
|
||||||
"intake-delivery-checklist-completed": "",
|
"intake-delivery-checklist-completed": "",
|
||||||
"job-added-to-production": "",
|
"job-added-to-production": "",
|
||||||
"job-assigned-to-me": "",
|
"job-assigned-to-me": "",
|
||||||
@@ -3259,6 +3299,7 @@
|
|||||||
"information": "",
|
"information": "",
|
||||||
"layout": "",
|
"layout": "",
|
||||||
"statistics": {
|
"statistics": {
|
||||||
|
"exclude_suspended": "",
|
||||||
"jobs_in_production": "",
|
"jobs_in_production": "",
|
||||||
"tasks_in_production": "",
|
"tasks_in_production": "",
|
||||||
"tasks_in_view": "",
|
"tasks_in_view": "",
|
||||||
@@ -3750,11 +3791,11 @@
|
|||||||
"jobhours": "",
|
"jobhours": "",
|
||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
"payrollclaimedtasks": "",
|
|
||||||
"payout_methods": {
|
"payout_methods": {
|
||||||
"commission": "",
|
"commission": "",
|
||||||
"hourly": ""
|
"hourly": ""
|
||||||
},
|
},
|
||||||
|
"payrollclaimedtasks": "",
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
"billmarkforreexport": "",
|
"billmarkforreexport": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
|
"billupdated": "",
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
@@ -136,6 +137,9 @@
|
|||||||
"jobintake": "",
|
"jobintake": "",
|
||||||
"jobinvoiced": "",
|
"jobinvoiced": "",
|
||||||
"jobioucreated": "",
|
"jobioucreated": "",
|
||||||
|
"joblineupdate": "",
|
||||||
|
"jobmanualcreate": "",
|
||||||
|
"jobmanuallineinsert": "",
|
||||||
"jobmodifylbradj": "",
|
"jobmodifylbradj": "",
|
||||||
"jobnoteadded": "",
|
"jobnoteadded": "",
|
||||||
"jobnotedeleted": "",
|
"jobnotedeleted": "",
|
||||||
@@ -151,7 +155,9 @@
|
|||||||
"tasks_deleted": "",
|
"tasks_deleted": "",
|
||||||
"tasks_uncompleted": "",
|
"tasks_uncompleted": "",
|
||||||
"tasks_undeleted": "",
|
"tasks_undeleted": "",
|
||||||
"tasks_updated": ""
|
"tasks_updated": "",
|
||||||
|
"timeticketcreated": "",
|
||||||
|
"timeticketupdated": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
@@ -317,14 +323,14 @@
|
|||||||
"addtemplate": "",
|
"addtemplate": "",
|
||||||
"newlaborrate": "",
|
"newlaborrate": "",
|
||||||
"newsalestaxcode": "",
|
"newsalestaxcode": "",
|
||||||
"save_shop_information": "",
|
|
||||||
"newstatus": "",
|
"newstatus": "",
|
||||||
|
"save_shop_information": "",
|
||||||
"testrender": ""
|
"testrender": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
"duplicate_job_status": "",
|
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
|
"duplicate_job_status": "",
|
||||||
"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": ""
|
"task_preset_allocation_exceeded": ""
|
||||||
@@ -364,6 +370,7 @@
|
|||||||
"cashierid": "",
|
"cashierid": "",
|
||||||
"default_journal": "",
|
"default_journal": "",
|
||||||
"disablebillwip": "",
|
"disablebillwip": "",
|
||||||
|
"disablecontact": "",
|
||||||
"disablecontactvehiclecreation": "",
|
"disablecontactvehiclecreation": "",
|
||||||
"dms_acctnumber": "",
|
"dms_acctnumber": "",
|
||||||
"dms_control_override": "",
|
"dms_control_override": "",
|
||||||
@@ -421,35 +428,6 @@
|
|||||||
"logo_img_path": "",
|
"logo_img_path": "",
|
||||||
"logo_img_path_height": "",
|
"logo_img_path_height": "",
|
||||||
"logo_img_path_width": "",
|
"logo_img_path_width": "",
|
||||||
"scoreboard_setup": {
|
|
||||||
"daily_body_target": "",
|
|
||||||
"daily_paint_target": "",
|
|
||||||
"ignore_blocked_days": "",
|
|
||||||
"last_number_working_days": "",
|
|
||||||
"production_target_hours": ""
|
|
||||||
},
|
|
||||||
"system_settings": {
|
|
||||||
"auto_email": {
|
|
||||||
"attach_pdf_to_email": "",
|
|
||||||
"from_emails": "",
|
|
||||||
"parts_order_cc": "",
|
|
||||||
"parts_return_slip_cc": ""
|
|
||||||
},
|
|
||||||
"job_costing": {
|
|
||||||
"paint_hour_split": "",
|
|
||||||
"paint_materials_hourly_cost_rate": "",
|
|
||||||
"prep_hour_split": "",
|
|
||||||
"shop_materials_hourly_cost_rate": "",
|
|
||||||
"target_touch_time": "",
|
|
||||||
"use_paint_scale_data": ""
|
|
||||||
},
|
|
||||||
"local_media_server": {
|
|
||||||
"enabled": "",
|
|
||||||
"http_path": "",
|
|
||||||
"network_path": "",
|
|
||||||
"token": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"md_categories": "",
|
"md_categories": "",
|
||||||
"md_ccc_rates": "",
|
"md_ccc_rates": "",
|
||||||
"md_classes": "",
|
"md_classes": "",
|
||||||
@@ -457,6 +435,7 @@
|
|||||||
"md_email_cc": "",
|
"md_email_cc": "",
|
||||||
"md_from_emails": "",
|
"md_from_emails": "",
|
||||||
"md_functionality_toggles": {
|
"md_functionality_toggles": {
|
||||||
|
"enhanced_early_ros": "",
|
||||||
"parts_queue_toggle": ""
|
"parts_queue_toggle": ""
|
||||||
},
|
},
|
||||||
"md_hour_split": {
|
"md_hour_split": {
|
||||||
@@ -705,6 +684,13 @@
|
|||||||
},
|
},
|
||||||
"schedule_end_time": "",
|
"schedule_end_time": "",
|
||||||
"schedule_start_time": "",
|
"schedule_start_time": "",
|
||||||
|
"scoreboard_setup": {
|
||||||
|
"daily_body_target": "",
|
||||||
|
"daily_paint_target": "",
|
||||||
|
"ignore_blocked_days": "",
|
||||||
|
"last_number_working_days": "",
|
||||||
|
"production_target_hours": ""
|
||||||
|
},
|
||||||
"shopname": "",
|
"shopname": "",
|
||||||
"speedprint": {
|
"speedprint": {
|
||||||
"id": "",
|
"id": "",
|
||||||
@@ -751,6 +737,28 @@
|
|||||||
"production_statuses": "",
|
"production_statuses": "",
|
||||||
"ready_statuses": ""
|
"ready_statuses": ""
|
||||||
},
|
},
|
||||||
|
"system_settings": {
|
||||||
|
"auto_email": {
|
||||||
|
"attach_pdf_to_email": "",
|
||||||
|
"from_emails": "",
|
||||||
|
"parts_order_cc": "",
|
||||||
|
"parts_return_slip_cc": ""
|
||||||
|
},
|
||||||
|
"job_costing": {
|
||||||
|
"paint_hour_split": "",
|
||||||
|
"paint_materials_hourly_cost_rate": "",
|
||||||
|
"prep_hour_split": "",
|
||||||
|
"shop_materials_hourly_cost_rate": "",
|
||||||
|
"target_touch_time": "",
|
||||||
|
"use_paint_scale_data": ""
|
||||||
|
},
|
||||||
|
"local_media_server": {
|
||||||
|
"enabled": "",
|
||||||
|
"http_path": "",
|
||||||
|
"network_path": "",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"target_touchtime": "",
|
"target_touchtime": "",
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"tt_allow_post_to_invoiced": "",
|
"tt_allow_post_to_invoiced": "",
|
||||||
@@ -770,6 +778,7 @@
|
|||||||
"alljobstatuses": "",
|
"alljobstatuses": "",
|
||||||
"allopenjobstatuses": "",
|
"allopenjobstatuses": "",
|
||||||
"apptcolors": "",
|
"apptcolors": "",
|
||||||
|
"autoemail": "",
|
||||||
"businessinformation": "",
|
"businessinformation": "",
|
||||||
"checklists": "",
|
"checklists": "",
|
||||||
"consent_settings": "",
|
"consent_settings": "",
|
||||||
@@ -777,7 +786,6 @@
|
|||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
"defaultprofitsmapping": "",
|
"defaultprofitsmapping": "",
|
||||||
"dms_setup": "",
|
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
@@ -792,10 +800,11 @@
|
|||||||
"rr_dealerid": "",
|
"rr_dealerid": "",
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"dms_setup": "",
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
"employee_teams": "",
|
|
||||||
"employee_options": "",
|
"employee_options": "",
|
||||||
"employee_rates": "",
|
"employee_rates": "",
|
||||||
|
"employee_teams": "",
|
||||||
"employee_vacation": "",
|
"employee_vacation": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
@@ -806,21 +815,22 @@
|
|||||||
"intakechecklist": "",
|
"intakechecklist": "",
|
||||||
"intellipay_cash_discount": "",
|
"intellipay_cash_discount": "",
|
||||||
"job_status_options": "",
|
"job_status_options": "",
|
||||||
|
"jobcosting": "",
|
||||||
"jobstatuses": "",
|
"jobstatuses": "",
|
||||||
|
"jump_to_section": "",
|
||||||
"laborrates": "",
|
"laborrates": "",
|
||||||
"licensing": "",
|
"licensing": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"md_parts_scan": "",
|
"md_parts_scan": "",
|
||||||
"md_ro_guard": "",
|
"md_ro_guard": "",
|
||||||
"md_ro_guard_options": "",
|
"md_ro_guard_options": "",
|
||||||
"md_tasks_presets": "",
|
"md_tasks_presets": "",
|
||||||
"task_preset_options": "",
|
|
||||||
"md_to_emails": "",
|
"md_to_emails": "",
|
||||||
"md_to_emails_emails": "",
|
"md_to_emails_emails": "",
|
||||||
"messagingpresets": "",
|
"messagingpresets": "",
|
||||||
"notification_options": "",
|
|
||||||
"notemplatesavailable": "",
|
"notemplatesavailable": "",
|
||||||
"notespresets": "",
|
"notespresets": "",
|
||||||
"jump_to_section": "",
|
"notification_options": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
@@ -857,9 +867,6 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
"autoemail": "",
|
|
||||||
"jobcosting": "",
|
|
||||||
"localmediaserver": "",
|
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
@@ -871,6 +878,7 @@
|
|||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
|
"task_preset_options": "",
|
||||||
"workingdays": ""
|
"workingdays": ""
|
||||||
},
|
},
|
||||||
"operations": {
|
"operations": {
|
||||||
@@ -1215,6 +1223,7 @@
|
|||||||
"confirmdelete": "",
|
"confirmdelete": "",
|
||||||
"doctype": "",
|
"doctype": "",
|
||||||
"dragtoupload": "",
|
"dragtoupload": "",
|
||||||
|
"greyscale": "Niveaux de gris",
|
||||||
"newjobid": "",
|
"newjobid": "",
|
||||||
"openinexplorer": "",
|
"openinexplorer": "",
|
||||||
"optimizedimage": "",
|
"optimizedimage": "",
|
||||||
@@ -1345,6 +1354,31 @@
|
|||||||
"unique_employee_number": ""
|
"unique_employee_number": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esignature": {
|
||||||
|
"actions": {
|
||||||
|
"delete": "",
|
||||||
|
"distribute": "",
|
||||||
|
"redistribute": "",
|
||||||
|
"upload_document": "Upload Document for E-Sign",
|
||||||
|
"view": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"no_token": "",
|
||||||
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"completed": "",
|
||||||
|
"completed_at": "",
|
||||||
|
"created_at": "",
|
||||||
|
"external_document_id": "",
|
||||||
|
"opened": "",
|
||||||
|
"rejected": "",
|
||||||
|
"status": "",
|
||||||
|
"title": "",
|
||||||
|
"updated_at": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"eula": {
|
"eula": {
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"accept": "Accept EULA"
|
"accept": "Accept EULA"
|
||||||
@@ -1460,8 +1494,8 @@
|
|||||||
"beta": "",
|
"beta": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"changelog": "",
|
"changelog": "",
|
||||||
"click_to_begin": "",
|
|
||||||
"clear": "",
|
"clear": "",
|
||||||
|
"click_to_begin": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
"date": "",
|
"date": "",
|
||||||
@@ -1779,9 +1813,9 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addpayer": "",
|
|
||||||
"addDocuments": "Ajouter des documents de travail",
|
"addDocuments": "Ajouter des documents de travail",
|
||||||
"addNote": "Ajouter une note",
|
"addNote": "Ajouter une note",
|
||||||
|
"addpayer": "",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
"addtoproduction": "",
|
"addtoproduction": "",
|
||||||
"addtoscoreboard": "",
|
"addtoscoreboard": "",
|
||||||
@@ -1958,6 +1992,7 @@
|
|||||||
"ded_status": "Statut de franchise",
|
"ded_status": "Statut de franchise",
|
||||||
"depreciation_taxes": "Amortissement / taxes",
|
"depreciation_taxes": "Amortissement / taxes",
|
||||||
"dms": {
|
"dms": {
|
||||||
|
"IsARCustomer": "",
|
||||||
"address": "",
|
"address": "",
|
||||||
"advisor": "",
|
"advisor": "",
|
||||||
"amount": "",
|
"amount": "",
|
||||||
@@ -2308,6 +2343,8 @@
|
|||||||
"duplicateconfirm": "",
|
"duplicateconfirm": "",
|
||||||
"emailaudit": "",
|
"emailaudit": "",
|
||||||
"employeeassignments": "",
|
"employeeassignments": "",
|
||||||
|
"esignature_imex": "",
|
||||||
|
"esignature_rome": "",
|
||||||
"estimatelines": "",
|
"estimatelines": "",
|
||||||
"estimator": "",
|
"estimator": "",
|
||||||
"existing_jobs": "Emplois existants",
|
"existing_jobs": "Emplois existants",
|
||||||
@@ -2747,6 +2784,9 @@
|
|||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
"bill-posted": "",
|
"bill-posted": "",
|
||||||
"critical-parts-status-changed": "",
|
"critical-parts-status-changed": "",
|
||||||
|
"esign-document-completed": "E-Sign Document Completed",
|
||||||
|
"esign-document-opened": "E-Sign Document Opened",
|
||||||
|
"esign-document-upload-failed": "E-Sign Document Upload Failed",
|
||||||
"intake-delivery-checklist-completed": "",
|
"intake-delivery-checklist-completed": "",
|
||||||
"job-added-to-production": "",
|
"job-added-to-production": "",
|
||||||
"job-assigned-to-me": "",
|
"job-assigned-to-me": "",
|
||||||
@@ -3259,6 +3299,7 @@
|
|||||||
"information": "",
|
"information": "",
|
||||||
"layout": "",
|
"layout": "",
|
||||||
"statistics": {
|
"statistics": {
|
||||||
|
"exclude_suspended": "",
|
||||||
"jobs_in_production": "",
|
"jobs_in_production": "",
|
||||||
"tasks_in_production": "",
|
"tasks_in_production": "",
|
||||||
"tasks_in_view": "",
|
"tasks_in_view": "",
|
||||||
@@ -3750,11 +3791,11 @@
|
|||||||
"jobhours": "",
|
"jobhours": "",
|
||||||
"lunch": "",
|
"lunch": "",
|
||||||
"new": "",
|
"new": "",
|
||||||
"payrollclaimedtasks": "",
|
|
||||||
"payout_methods": {
|
"payout_methods": {
|
||||||
"commission": "",
|
"commission": "",
|
||||||
"hourly": ""
|
"hourly": ""
|
||||||
},
|
},
|
||||||
|
"payrollclaimedtasks": "",
|
||||||
"pmbreak": "",
|
"pmbreak": "",
|
||||||
"pmshift": "",
|
"pmshift": "",
|
||||||
"shift": "",
|
"shift": "",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
//import { store } from "../redux/store";
|
//import { store } from "../redux/store";
|
||||||
|
import { DMS_MAP } from "./dmsUtils";
|
||||||
import InstanceRenderManager from "./instanceRenderMgr";
|
import InstanceRenderManager from "./instanceRenderMgr";
|
||||||
|
|
||||||
export const EmailSettings = {
|
export const EmailSettings = {
|
||||||
@@ -570,7 +571,8 @@ export const TemplateList = (type, context) => {
|
|||||||
key: "dms_posting_sheet",
|
key: "dms_posting_sheet",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
group: "financial",
|
group: "financial",
|
||||||
dms: true
|
dms: true,
|
||||||
|
excludedDmsModes: [DMS_MAP.reynolds]
|
||||||
},
|
},
|
||||||
worksheet_sorted_by_team: {
|
worksheet_sorted_by_team: {
|
||||||
title: i18n.t("printcenter.jobs.worksheet_sorted_by_team"),
|
title: i18n.t("printcenter.jobs.worksheet_sorted_by_team"),
|
||||||
|
|||||||
7
client/src/utils/esignature.js
Normal file
7
client/src/utils/esignature.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const hasDocumensoApiKey = (bodyshop) => {
|
||||||
|
if (typeof bodyshop?.documenso_api_key === "string") {
|
||||||
|
return bodyshop.documenso_api_key.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(bodyshop?.documenso_api_key);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @description This file contains the scenarios for job notifications.
|
* @description This file contains the scenarios for job notifications.
|
||||||
* @type {string[]}
|
* @type {string[]}
|
||||||
*/
|
*/
|
||||||
const notificationScenarios = [
|
const baseNotificationScenarios = [
|
||||||
"job-assigned-to-me",
|
"job-assigned-to-me",
|
||||||
"bill-posted",
|
"bill-posted",
|
||||||
"critical-parts-status-changed",
|
"critical-parts-status-changed",
|
||||||
@@ -20,4 +20,26 @@ const notificationScenarios = [
|
|||||||
// "supplement-imported", // Disabled for now
|
// "supplement-imported", // Disabled for now
|
||||||
];
|
];
|
||||||
|
|
||||||
export { notificationScenarios };
|
const esignNotificationScenarios = [
|
||||||
|
"esign-document-opened",
|
||||||
|
"esign-document-completed",
|
||||||
|
"esign-document-upload-failed"
|
||||||
|
];
|
||||||
|
|
||||||
|
const notificationScenarios = [...baseNotificationScenarios, ...esignNotificationScenarios];
|
||||||
|
|
||||||
|
const getNotificationScenarios = ({ includeEsign = true } = {}) =>
|
||||||
|
includeEsign ? notificationScenarios : baseNotificationScenarios;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default channel preferences for e-sign document notifications. By default, all e-sign related notifications will be
|
||||||
|
* sent via the app, but not via email or FCM. These defaults can be overridden by user preferences.
|
||||||
|
* @type {{"esign-document-opened": {app: boolean, email: boolean, fcm: boolean}, "esign-document-completed": {app: boolean, email: boolean, fcm: boolean}, "esign-document-upload-failed": {app: boolean, email: boolean, fcm: boolean}}}
|
||||||
|
*/
|
||||||
|
const notificationScenarioDefaults = {
|
||||||
|
"esign-document-opened": { app: true, email: false, fcm: false },
|
||||||
|
"esign-document-completed": { app: true, email: false, fcm: false },
|
||||||
|
"esign-document-upload-failed": { app: true, email: false, fcm: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
export { esignNotificationScenarios, getNotificationScenarios, notificationScenarios, notificationScenarioDefaults };
|
||||||
|
|||||||
28
client/src/utils/replaceAccents.js
Normal file
28
client/src/utils/replaceAccents.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export const 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;
|
||||||
|
};
|
||||||
24
documenso/cert/certificate.crt
Normal file
24
documenso/cert/certificate.crt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID9zCCAt+gAwIBAgIUTB4OhIqfXvT0mBKHwYAwDPq79ygwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgYoxCzAJBgNVBAYTAkNBMQswCQYDVQQIDAJCQzESMBAGA1UEBwwJVmFuY291
|
||||||
|
dmVyMRowGAYDVQQKDBFJbUVYIFN5c3RlbXMgSW5jLjEXMBUGA1UEAwwOaW1leHN5
|
||||||
|
c3RlbXMuY2ExJTAjBgkqhkiG9w0BCQEWFmNvbnRhY3RAaW1leHN5c3RlbXMuY2Ew
|
||||||
|
HhcNMjYwNDEzMjAxMDIzWhcNMzYwNDEwMjAxMDIzWjCBijELMAkGA1UEBhMCQ0Ex
|
||||||
|
CzAJBgNVBAgMAkJDMRIwEAYDVQQHDAlWYW5jb3V2ZXIxGjAYBgNVBAoMEUltRVgg
|
||||||
|
U3lzdGVtcyBJbmMuMRcwFQYDVQQDDA5pbWV4c3lzdGVtcy5jYTElMCMGCSqGSIb3
|
||||||
|
DQEJARYWY29udGFjdEBpbWV4c3lzdGVtcy5jYTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||||
|
ggEPADCCAQoCggEBAPE+5bcnfYsMyLzJr50bzpHHP8I+cdSkvu7lwGysPZCCxi4Z
|
||||||
|
vkIDq4Q5xDa3ZZCeNZ9feELqm9ZjWpnaZj4CMbXMDpIucZHQJC9USCGavYhzNYu2
|
||||||
|
G3IU7D834jd8GkwGMQuXkGiuQmQssIZIKfX+MaZ0KKrh8gJbxXZOfCp3fdYOnFPq
|
||||||
|
BFCR0N/gTbeRboq36dG4vo1FanDLGroMS7FycGjyUTQv3CTWkGAOAPGQVrGZgvYM
|
||||||
|
DtFr+7M2J/KCbUMobK0uc1scAjLgetXknzVPU3qA66F3Hi7oWykoFX8m9oX/OJnK
|
||||||
|
/Gt8rIjRMOyQSK7dKT7qXCxgQVQnqHbyUCX4WUkCAwEAAaNTMFEwHQYDVR0OBBYE
|
||||||
|
FIRKLjeI+adC7yNg6cSDj72Kej11MB8GA1UdIwQYMBaAFIRKLjeI+adC7yNg6cSD
|
||||||
|
j72Kej11MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAHCSjlG
|
||||||
|
bo5miEfisKffPyfzufBIhOLLORasuFQ3gVKBU32JytuoflABfcqy3prgZxbFLMB2
|
||||||
|
fDcSImKuOtt79OMeMlA+ptfkWuOpFMqL2j6BilzjJ/MAlPAZlZmmuLh/fPj3lbMD
|
||||||
|
QQds/YhSmZcTdRX8seQslnYq1AT7629BDbpCjjL3pRkntnePR7u8tgb28Pm8Vl3S
|
||||||
|
uCnGS/mMxrS/7z+QnaDi1N/nyIwa2bQtGmsoMn+CzuUUjyMD4TYbdUJv+fca8/tR
|
||||||
|
zezNEHcpBCKGGgZRowhifJwEoel0M1iEo8UYy5eFPDF8CoRGRIH7QSaduCfnej06
|
||||||
|
KLtevL/vyhUpTMA=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
documenso/cert/certificate.p12
Normal file
BIN
documenso/cert/certificate.p12
Normal file
Binary file not shown.
72
documenso/docker-compose.yml
Normal file
72
documenso/docker-compose.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
name: documenso-production
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:?err}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:?err}
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
documenso:
|
||||||
|
image: documenso/documenso:latest
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-3000}
|
||||||
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?err}
|
||||||
|
- NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY:?err}
|
||||||
|
- NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY:?err}
|
||||||
|
- NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID}
|
||||||
|
- NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET}
|
||||||
|
- NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:?err}
|
||||||
|
- NEXT_PRIVATE_INTERNAL_WEBAPP_URL=${NEXT_PRIVATE_INTERNAL_WEBAPP_URL:-http://localhost:$PORT}
|
||||||
|
- NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:?err}
|
||||||
|
- NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL:-${NEXT_PRIVATE_DATABASE_URL}}
|
||||||
|
- NEXT_PUBLIC_UPLOAD_TRANSPORT=${NEXT_PUBLIC_UPLOAD_TRANSPORT:-database}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_ENDPOINT=${NEXT_PRIVATE_UPLOAD_ENDPOINT}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=${NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_REGION=${NEXT_PRIVATE_UPLOAD_REGION}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_BUCKET=${NEXT_PRIVATE_UPLOAD_BUCKET}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=${NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID}
|
||||||
|
- NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=${NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY}
|
||||||
|
- NEXT_PRIVATE_SMTP_TRANSPORT=${NEXT_PRIVATE_SMTP_TRANSPORT:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_HOST=${NEXT_PRIVATE_SMTP_HOST}
|
||||||
|
- NEXT_PRIVATE_SMTP_PORT=${NEXT_PRIVATE_SMTP_PORT}
|
||||||
|
- NEXT_PRIVATE_SMTP_USERNAME=${NEXT_PRIVATE_SMTP_USERNAME}
|
||||||
|
- NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD}
|
||||||
|
- NEXT_PRIVATE_SMTP_APIKEY_USER=${NEXT_PRIVATE_SMTP_APIKEY_USER}
|
||||||
|
- NEXT_PRIVATE_SMTP_APIKEY=${NEXT_PRIVATE_SMTP_APIKEY}
|
||||||
|
- NEXT_PRIVATE_SMTP_SECURE=${NEXT_PRIVATE_SMTP_SECURE}
|
||||||
|
- NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=${NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS}
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_NAME=${NEXT_PRIVATE_SMTP_FROM_NAME:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS:?err}
|
||||||
|
- NEXT_PRIVATE_SMTP_SERVICE=${NEXT_PRIVATE_SMTP_SERVICE}
|
||||||
|
- NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=${NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=${NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR}
|
||||||
|
- NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=${NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY}
|
||||||
|
- NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT}
|
||||||
|
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||||
|
- NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP}
|
||||||
|
- NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=${NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS}
|
||||||
|
- NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH:-/opt/documenso/cert.p12}
|
||||||
|
- NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE}
|
||||||
|
- NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=${NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS}
|
||||||
|
ports:
|
||||||
|
- ${PORT:-3000}:${PORT:-3000}
|
||||||
|
volumes:
|
||||||
|
- /opt/documenso/cert.p12:/opt/documenso/cert.p12:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
45
documenso/terraform/.terraform.lock.hcl
generated
Normal file
45
documenso/terraform/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file is maintained automatically by "terraform init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/aws" {
|
||||||
|
version = "6.38.0"
|
||||||
|
constraints = "~> 6.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
|
||||||
|
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
|
||||||
|
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
|
||||||
|
"zh:3a31baabf7aea7aa7669f5a3d76f3445e0e6cce5e9aea0279992765c0df12aee",
|
||||||
|
"zh:4c1908e62040dbc9901d4426ffb253f53e5dae9e3e1a9125311291ee265c8d8c",
|
||||||
|
"zh:550f4789f5f5b00e16118d4c17770be3ef4535d6b6928af1cf91ebd30f2c263b",
|
||||||
|
"zh:6537b7b70bf2c127771b0b84e4b726c834d10666b6104f017edae50c67ebae37",
|
||||||
|
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||||
|
"zh:af2f9cea0c8bdf5b2a2391f2d179a946c117196f7c829b919673cae3b71d2943",
|
||||||
|
"zh:c53ffa685381aa4e73158fd9f529239f95938dea330e7aca0b32e7b2a1210432",
|
||||||
|
"zh:d0995e1d64a7ec8bbc79fc3fbec3749f989e07f211a318705c37cd6a7c7d19e4",
|
||||||
|
"zh:d2348ffcffc1282983d7a5838dd5d61f372152fe6c0d10868cd6473352318750",
|
||||||
|
"zh:e449312efb73e4747165e689302a68a1df8ba5755e7f59097069acf82c94f011",
|
||||||
|
"zh:ec3a538d264ef79380e56fdf107ffb6c0446814f07fc5890c36855fe1e03196b",
|
||||||
|
"zh:f441e69699b22e32c96a8cdd3bbe694ed302c0dcfe867cd9bd683a16df362714",
|
||||||
|
"zh:f6f8eaa605ff902234d7e9bdab4fda977185fce14f8576f7b622c914c7d98008",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/random" {
|
||||||
|
version = "3.8.1"
|
||||||
|
constraints = "~> 3.6"
|
||||||
|
hashes = [
|
||||||
|
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
|
||||||
|
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
|
||||||
|
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
|
||||||
|
"zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57",
|
||||||
|
"zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0",
|
||||||
|
"zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66",
|
||||||
|
"zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9",
|
||||||
|
"zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05",
|
||||||
|
"zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8",
|
||||||
|
"zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b",
|
||||||
|
"zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699",
|
||||||
|
]
|
||||||
|
}
|
||||||
60
documenso/terraform/README.md
Normal file
60
documenso/terraform/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Documenso on AWS
|
||||||
|
|
||||||
|
This Terraform stack deploys Documenso to AWS in `ca-central-1` using:
|
||||||
|
|
||||||
|
- ECS Fargate for the application tier
|
||||||
|
- RDS PostgreSQL for the database tier
|
||||||
|
- S3 for document uploads and signed PDFs
|
||||||
|
- Application Load Balancer with ACM-managed TLS
|
||||||
|
- Route53 DNS for `esignature.imex.online`
|
||||||
|
- Optional SES domain identity and DKIM management for outbound email
|
||||||
|
- Secrets Manager for generated application secrets, SMTP credentials, and the optional Documenso signing certificate
|
||||||
|
- AWS WAF with a basic managed rule set, rate limiting, and an allowlist for trusted IPv4 CIDRs
|
||||||
|
- CloudWatch alarms for ALB, ECS, and RDS health indicators
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
This is the most practical fit for your Docker Compose workload if you want a balance of cost efficiency, managed operations, and scaling:
|
||||||
|
|
||||||
|
- Fargate gives you horizontal scaling without managing EC2 hosts.
|
||||||
|
- RDS PostgreSQL is simpler and cheaper than Aurora for a single Documenso workload.
|
||||||
|
- S3-backed uploads are better for production scale and keep document growth out of PostgreSQL.
|
||||||
|
- The database stays private; the ALB is public.
|
||||||
|
- The ECS tasks run in public subnets to avoid a NAT gateway charge. Inbound access is still restricted to the ALB security group.
|
||||||
|
- HTTPS is terminated by the ALB using ACM. The Documenso self-signed `.p12` certificate is separate and is used for document signing, not browser TLS.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `main.tf`: core infrastructure
|
||||||
|
- `variables.tf`: configurable inputs
|
||||||
|
- `outputs.tf`: useful deployment outputs
|
||||||
|
- `terraform.tfvars.example`: example input values
|
||||||
|
|
||||||
|
## Assumptions built into this stack
|
||||||
|
|
||||||
|
1. Your DNS for `imex.online` is hosted in Route53.
|
||||||
|
2. You want Multi-AZ RDS enabled from the start for database availability.
|
||||||
|
3. You are comfortable starting with `documenso/documenso:latest`. For repeatable deployments, pin a version or digest after your first rollout.
|
||||||
|
4. You will provide SES SMTP credentials. Terraform does not derive SMTP passwords for you.
|
||||||
|
5. SES identity and DKIM might already be managed outside this stack. By default, this Terraform does not attempt to create them.
|
||||||
|
6. You will provide a base64-encoded PKCS#12 signing certificate and passphrase if you want document signing enabled immediately. This stack injects those values through Secrets Manager instead of mounting a host file.
|
||||||
|
7. You are comfortable with Terraform creating a dedicated IAM user and access key for Documenso S3 uploads because Documenso documents explicit S3 credentials for the upload backend.
|
||||||
|
8. You want Terraform destroy protection enabled for both the database and the uploads bucket.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in the SMTP values.
|
||||||
|
2. If you want Documenso signing enabled, add `signing_certificate_base64` and `signing_certificate_passphrase`.
|
||||||
|
3. Optionally set `upload_bucket_name` if you want a specific S3 bucket name.
|
||||||
|
4. Set `manage_ses_resources = true` only if you want this stack to own SES identity verification and DKIM records.
|
||||||
|
5. Set `waf_bypass_ipv4_cidrs` with any public `/32` addresses that should bypass WAF inspection. The VPC CIDR is already allowlisted automatically.
|
||||||
|
6. Run `terraform init`.
|
||||||
|
7. Run `terraform plan`.
|
||||||
|
8. Run `terraform apply`.
|
||||||
|
|
||||||
|
## Recommended first production adjustments
|
||||||
|
|
||||||
|
1. Pin the Documenso image to a tested version or digest.
|
||||||
|
2. Wire `alarm_actions` to an SNS topic, PagerDuty bridge, or your on-call system so alarms notify someone.
|
||||||
|
3. Expand the WAF rule set if you need more aggressive filtering later.
|
||||||
|
4. Add CloudWatch alarms on ECS 5xx errors, ALB target health, and RDS CPU/storage.
|
||||||
1052
documenso/terraform/main.tf
Normal file
1052
documenso/terraform/main.tf
Normal file
File diff suppressed because it is too large
Load Diff
44
documenso/terraform/outputs.tf
Normal file
44
documenso/terraform/outputs.tf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
output "application_url" {
|
||||||
|
description = "Public URL for the Documenso deployment."
|
||||||
|
value = "https://${var.domain_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "load_balancer_dns_name" {
|
||||||
|
description = "DNS name assigned to the application load balancer."
|
||||||
|
value = aws_lb.this.dns_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "database_endpoint" {
|
||||||
|
description = "RDS PostgreSQL endpoint for the application."
|
||||||
|
value = aws_db_instance.postgres.address
|
||||||
|
}
|
||||||
|
|
||||||
|
output "postgres_engine_version" {
|
||||||
|
description = "Resolved PostgreSQL engine version deployed to RDS."
|
||||||
|
value = aws_db_instance.postgres.engine_version
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ecs_cluster_name" {
|
||||||
|
description = "ECS cluster name running the Documenso service."
|
||||||
|
value = aws_ecs_cluster.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secrets_manager_secret_name" {
|
||||||
|
description = "Secrets Manager secret that stores generated and supplied application secrets."
|
||||||
|
value = aws_secretsmanager_secret.app.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ses_identity_domain" {
|
||||||
|
description = "SES domain used for outbound mail."
|
||||||
|
value = local.ses_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
output "upload_bucket_name" {
|
||||||
|
description = "S3 bucket used for Documenso uploads."
|
||||||
|
value = aws_s3_bucket.uploads.bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
output "waf_web_acl_arn" {
|
||||||
|
description = "ARN of the WAF web ACL attached to the ALB."
|
||||||
|
value = aws_wafv2_web_acl.this.arn
|
||||||
|
}
|
||||||
3994
documenso/terraform/terraform.tfstate
Normal file
3994
documenso/terraform/terraform.tfstate
Normal file
File diff suppressed because one or more lines are too long
3977
documenso/terraform/terraform.tfstate.backup
Normal file
3977
documenso/terraform/terraform.tfstate.backup
Normal file
File diff suppressed because one or more lines are too long
24
documenso/terraform/terraform.tfvars.example
Normal file
24
documenso/terraform/terraform.tfvars.example
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
aws_region = "ca-central-1"
|
||||||
|
domain_name = "esignature.imex.online"
|
||||||
|
hosted_zone_name = "imex.online"
|
||||||
|
documenso_image = "documenso/documenso:latest"
|
||||||
|
smtp_username = "AKIA2MRSPON3O6PRVUPE"
|
||||||
|
smtp_password = "pw"
|
||||||
|
smtp_from_address = "no-reply@imex.online"
|
||||||
|
manage_ses_resources = false
|
||||||
|
ses_identity_domain = "imex.online"
|
||||||
|
app_secret_name = "documenso/esignature-imex-online/app"
|
||||||
|
# signing_certificate_base64 = "MII...base64-encoded-p12..."
|
||||||
|
# signing_certificate_passphrase = "replace-with-your-p12-passphrase"
|
||||||
|
# upload_bucket_name = "esignature-imex-online-documenso"
|
||||||
|
|
||||||
|
# Optional tuning
|
||||||
|
# desired_count = 2
|
||||||
|
# max_count = 6
|
||||||
|
waf_bypass_ipv4_cidrs = ["203.0.113.10/32"]
|
||||||
|
db_instance_class = "db.t4g.micro"
|
||||||
|
db_publicly_accessible = true
|
||||||
|
db_allowed_cidrs = ["64.46.30.40/32"]
|
||||||
|
disable_signup = false
|
||||||
|
# allowed_signup_domains = "imex.online"
|
||||||
|
# alarm_actions = ["arn:aws:sns:ca-central-1:123456789012:ops-alerts"]
|
||||||
318
documenso/terraform/variables.tf
Normal file
318
documenso/terraform/variables.tf
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
variable "aws_region" {
|
||||||
|
description = "AWS region for the deployment."
|
||||||
|
type = string
|
||||||
|
default = "ca-central-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "project_name" {
|
||||||
|
description = "Logical name used to prefix created resources."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain_name" {
|
||||||
|
description = "Fully qualified domain name for the application."
|
||||||
|
type = string
|
||||||
|
default = "esignature.imex.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hosted_zone_name" {
|
||||||
|
description = "Public Route53 hosted zone that contains the application hostname."
|
||||||
|
type = string
|
||||||
|
default = "imex.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ses_identity_domain" {
|
||||||
|
description = "Domain used for SES. Defaults to the hosted zone when null. If manage_ses_resources is false, this is informational and used only for outputs/documentation."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "manage_ses_resources" {
|
||||||
|
description = "Whether this Terraform stack should create and manage the SES domain identity, verification record, and DKIM records. Disable this when SES is already configured elsewhere."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "documenso_image" {
|
||||||
|
description = "Container image for Documenso. Default keeps you on the latest published image."
|
||||||
|
type = string
|
||||||
|
default = "documenso/documenso:latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_port" {
|
||||||
|
description = "Container port exposed by Documenso."
|
||||||
|
type = number
|
||||||
|
default = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "upload_bucket_name" {
|
||||||
|
description = "Optional S3 bucket name for Documenso uploads. If null, Terraform generates a globally unique name based on account and region."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_versioning_enabled" {
|
||||||
|
description = "Enable S3 object versioning for uploaded documents."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "document_size_upload_limit_mb" {
|
||||||
|
description = "Upload size limit shown in the Documenso UI, in MB."
|
||||||
|
type = number
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vpc_cidr" {
|
||||||
|
description = "CIDR block used for the VPC."
|
||||||
|
type = string
|
||||||
|
default = "10.42.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "fargate_cpu" {
|
||||||
|
description = "Fargate CPU units for the task."
|
||||||
|
type = number
|
||||||
|
default = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "fargate_memory" {
|
||||||
|
description = "Fargate memory in MiB for the task."
|
||||||
|
type = number
|
||||||
|
default = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "desired_count" {
|
||||||
|
description = "Initial number of running Documenso tasks."
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "min_count" {
|
||||||
|
description = "Minimum number of tasks for autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "max_count" {
|
||||||
|
description = "Maximum number of tasks for autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cpu_target_utilization" {
|
||||||
|
description = "Target average CPU utilization for ECS autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 65
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "memory_target_utilization" {
|
||||||
|
description = "Target average memory utilization for ECS autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_major_version" {
|
||||||
|
description = "Preferred PostgreSQL major version. Terraform resolves the latest matching minor release supported by AWS."
|
||||||
|
type = string
|
||||||
|
default = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_name" {
|
||||||
|
description = "Initial PostgreSQL database name."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_username" {
|
||||||
|
description = "Master PostgreSQL username for the application."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_instance_class" {
|
||||||
|
description = "RDS instance class. Graviton classes are usually the best cost/performance option for Postgres."
|
||||||
|
type = string
|
||||||
|
default = "db.t4g.small"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_allocated_storage" {
|
||||||
|
description = "Initial allocated storage in GiB."
|
||||||
|
type = number
|
||||||
|
default = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_max_allocated_storage" {
|
||||||
|
description = "Maximum autoscaled storage in GiB."
|
||||||
|
type = number
|
||||||
|
default = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_backup_retention_days" {
|
||||||
|
description = "How many days of automated backups to retain."
|
||||||
|
type = number
|
||||||
|
default = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_multi_az" {
|
||||||
|
description = "Enable Multi-AZ for higher database availability at higher cost."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_deletion_protection" {
|
||||||
|
description = "Protect the database from accidental deletion."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_final_snapshot_on_destroy" {
|
||||||
|
description = "Create a final snapshot if the database is destroyed."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_publicly_accessible" {
|
||||||
|
description = "Whether the RDS instance should have a public endpoint. Requires database subnets with a route to the internet gateway."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_allowed_cidrs" {
|
||||||
|
description = "IPv4 CIDR blocks allowed to connect directly to PostgreSQL. Leave empty to disable direct public access."
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "disable_signup" {
|
||||||
|
description = "Disable public signup in Documenso."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allowed_signup_domains" {
|
||||||
|
description = "Optional comma-separated list of allowed email domains when signup is enabled."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_port" {
|
||||||
|
description = "SES SMTP endpoint port."
|
||||||
|
type = number
|
||||||
|
default = 587
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_secure" {
|
||||||
|
description = "Whether to use SMTPS. Keep false for SES on port 587 with STARTTLS."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_unsafe_ignore_tls" {
|
||||||
|
description = "Whether the application should ignore TLS issues when sending mail."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_username" {
|
||||||
|
description = "SES SMTP username."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_password" {
|
||||||
|
description = "SES SMTP password."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_from_name" {
|
||||||
|
description = "Display name used in outbound email."
|
||||||
|
type = string
|
||||||
|
default = "ImEX Sign"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_from_address" {
|
||||||
|
description = "Verified sender email address for SES."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "signing_certificate_base64" {
|
||||||
|
description = "Base64-encoded PKCS#12 signing certificate contents for Documenso. Leave empty to omit certificate injection."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "signing_certificate_passphrase" {
|
||||||
|
description = "Passphrase for the Documenso signing certificate. Leave empty to omit it."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_secret_name" {
|
||||||
|
description = "Secrets Manager secret name used for Documenso application secrets. Set this if a previous secret with the default name is pending deletion."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tags" {
|
||||||
|
description = "Additional tags applied to all supported resources."
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "waf_rate_limit" {
|
||||||
|
description = "Maximum requests per 5-minute window from a single IP before WAF blocks it."
|
||||||
|
type = number
|
||||||
|
default = 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "waf_bypass_ipv4_cidrs" {
|
||||||
|
description = "Additional IPv4 CIDR blocks that bypass the WAF. The VPC CIDR is always included automatically."
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "alarm_actions" {
|
||||||
|
description = "Optional list of SNS topic ARNs or other alarm actions to invoke when CloudWatch alarms fire."
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "alb_5xx_alarm_threshold" {
|
||||||
|
description = "Threshold for ALB 5xx count over a 5-minute period."
|
||||||
|
type = number
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ecs_cpu_alarm_threshold" {
|
||||||
|
description = "Threshold for average ECS CPU utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 85
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ecs_memory_alarm_threshold" {
|
||||||
|
description = "Threshold for average ECS memory utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 85
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rds_cpu_alarm_threshold" {
|
||||||
|
description = "Threshold for average RDS CPU utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rds_free_storage_alarm_threshold_bytes" {
|
||||||
|
description = "Alarm threshold for low RDS free storage, in bytes."
|
||||||
|
type = number
|
||||||
|
default = 5368709120
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "documenso_license_key" {
|
||||||
|
description = "Documenso license key. Not required for the free community edition, but required for enterprise features and support."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
@@ -956,6 +956,7 @@
|
|||||||
- created_at
|
- created_at
|
||||||
- default_adjustment_rate
|
- default_adjustment_rate
|
||||||
- deliverchecklist
|
- deliverchecklist
|
||||||
|
- documenso_api_key
|
||||||
- email
|
- email
|
||||||
- enforce_class
|
- enforce_class
|
||||||
- enforce_conversion_category
|
- enforce_conversion_category
|
||||||
@@ -1893,6 +1894,14 @@
|
|||||||
- name: job
|
- name: job
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on: jobid
|
foreign_key_constraint_on: jobid
|
||||||
|
array_relationships:
|
||||||
|
- name: esignature_documents
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: documentid
|
||||||
|
table:
|
||||||
|
name: esignature_documents
|
||||||
|
schema: public
|
||||||
insert_permissions:
|
insert_permissions:
|
||||||
- role: user
|
- role: user
|
||||||
permission:
|
permission:
|
||||||
@@ -2568,6 +2577,101 @@
|
|||||||
_eq: X-Hasura-User-Id
|
_eq: X-Hasura-User-Id
|
||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
|
- table:
|
||||||
|
name: esignature_documents
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: document
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: documentid
|
||||||
|
- name: job
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: jobid
|
||||||
|
insert_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- jobid
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
comment: ""
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- completed_at
|
||||||
|
- created_at
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- id
|
||||||
|
- jobid
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
comment: ""
|
||||||
|
update_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- completed
|
||||||
|
- completed_at
|
||||||
|
- created_at
|
||||||
|
- documentid
|
||||||
|
- external_document_id
|
||||||
|
- message
|
||||||
|
- opened
|
||||||
|
- recipients
|
||||||
|
- rejected
|
||||||
|
- status
|
||||||
|
- subject
|
||||||
|
- title
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
job:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
check: null
|
||||||
|
comment: ""
|
||||||
- table:
|
- table:
|
||||||
name: eula_acceptances
|
name: eula_acceptances
|
||||||
schema: public
|
schema: public
|
||||||
@@ -3466,6 +3570,13 @@
|
|||||||
table:
|
table:
|
||||||
name: email_audit_trail
|
name: email_audit_trail
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: esignature_documents
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: jobid
|
||||||
|
table:
|
||||||
|
name: esignature_documents
|
||||||
|
schema: public
|
||||||
- name: exportlogs
|
- name: exportlogs
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."esignature_documents";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE "public"."esignature_documents" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "external_document_id" text NOT NULL, "jobid" uuid NOT NULL, "status" text NOT NULL, "recipients" jsonb[] NOT NULL, "title" text NOT NULL, "subject" text NOT NULL, "message" text NOT NULL, "viewed" boolean NOT NULL DEFAULT false, "completed" boolean NOT NULL DEFAULT false, "documentid" uuid, "rejected" boolean NOT NULL DEFAULT false, "opened" boolean NOT NULL DEFAULT false, PRIMARY KEY ("id") , FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict);COMMENT ON TABLE "public"."esignature_documents" IS E'Tracking the lifecycle of esignature documents. ';
|
||||||
|
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
_new record;
|
||||||
|
BEGIN
|
||||||
|
_new := NEW;
|
||||||
|
_new."updated_at" = NOW();
|
||||||
|
RETURN _new;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
CREATE TRIGGER "set_public_esignature_documents_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."esignature_documents"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||||
|
COMMENT ON TRIGGER "set_public_esignature_documents_updated_at" ON "public"."esignature_documents"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."esignature_documents" drop constraint "esignature_documents_documentid_fkey";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
alter table "public"."esignature_documents"
|
||||||
|
add constraint "esignature_documents_documentid_fkey"
|
||||||
|
foreign key ("documentid")
|
||||||
|
references "public"."documents"
|
||||||
|
("id") on update restrict on delete restrict;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."esignature_documents" ALTER COLUMN "recipients" TYPE ARRAY;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "public"."esignature_documents" ALTER COLUMN "recipients" TYPE json[];
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."esignature_documents" add column "completed_at" timestamptz
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."esignature_documents" add column "completed_at" timestamptz
|
||||||
|
null;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
comment on column "public"."esignature_documents"."viewed" is E'Tracking the lifecycle of esignature documents. ';
|
||||||
|
alter table "public"."esignature_documents" alter column "viewed" set default false;
|
||||||
|
alter table "public"."esignature_documents" alter column "viewed" drop not null;
|
||||||
|
alter table "public"."esignature_documents" add column "viewed" bool;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."esignature_documents" drop column "viewed" cascade;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "documenso_api_key" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "documenso_api_key" text
|
||||||
|
null;
|
||||||
749
package-lock.json
generated
749
package-lock.json
generated
@@ -19,6 +19,8 @@
|
|||||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||||
"@aws-sdk/lib-storage": "^3.1020.0",
|
"@aws-sdk/lib-storage": "^3.1020.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1020.0",
|
"@aws-sdk/s3-request-presigner": "^3.1020.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",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"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",
|
||||||
|
"normalize-url": "^9.0.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
@@ -1360,6 +1363,18 @@
|
|||||||
"kuler": "^2.0.0"
|
"kuler": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@documenso/sdk-typescript": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@documenso/sdk-typescript/-/sdk-typescript-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-Emzd5j+v8tA8gxtL+M/svVuzSOKMZw3/U4bS8zRoagvQEqkt+XNU2JraPEAJzxTjf3ww6EnlURXydbglBmR7AQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mcp": "bin/mcp-server.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||||
@@ -2196,6 +2211,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hono/node-server": {
|
||||||
|
"version": "1.19.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||||
|
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.14.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"hono": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -2303,12 +2330,370 @@
|
|||||||
"url": "https://opencollective.com/js-sdsl"
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsreport/nodejs-client": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsreport/nodejs-client/-/nodejs-client-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-QWupUQzMzxWFvY+AlSdUZGlinJv4cKhYmVE9rIe+he7rn4B24tezFmNdnrDcTSFv3hj4x7sTNqpeHT0fItfs5Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "1.13.2",
|
||||||
|
"concat-stream": "2.0.0",
|
||||||
|
"mimic-response": "2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jsreport/nodejs-client/node_modules/axios": {
|
||||||
|
"version": "1.13.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||||
|
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jsreport/nodejs-client/node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@kurkle/color": {
|
"node_modules/@kurkle/color": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
|
"version": "1.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
|
||||||
|
"integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.19.9",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-spawn": "^7.0.5",
|
||||||
|
"eventsource": "^3.0.2",
|
||||||
|
"eventsource-parser": "^3.0.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"hono": "^4.11.4",
|
||||||
|
"jose": "^6.1.3",
|
||||||
|
"json-schema-typed": "^8.0.2",
|
||||||
|
"pkce-challenge": "^5.0.0",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"zod": "^3.25 || ^4.0",
|
||||||
|
"zod-to-json-schema": "^3.25.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@cfworker/json-schema": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/express": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.1",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/jose": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/qs": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||||
@@ -4183,6 +4568,45 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv-formats": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-formats/node_modules/ajv": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
@@ -4641,6 +5065,100 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.7.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.1",
|
||||||
|
"raw-body": "^3.0.1",
|
||||||
|
"type-is": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/qs": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boolbase": {
|
"node_modules/boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
@@ -6370,6 +6888,27 @@
|
|||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventsource-parser": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eventsource-parser": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expect-type": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
@@ -6426,6 +6965,24 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/body-parser": {
|
"node_modules/express/node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -6548,6 +7105,22 @@
|
|||||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fast-xml-builder": {
|
"node_modules/fast-xml-builder": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||||
@@ -7429,6 +8002,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hono": {
|
||||||
|
"version": "4.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
|
||||||
|
"integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hpagent": {
|
"node_modules/hpagent": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz",
|
||||||
@@ -7687,6 +8269,15 @@
|
|||||||
"url": "https://opencollective.com/ioredis"
|
"url": "https://opencollective.com/ioredis"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||||
|
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -7928,6 +8519,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
@@ -8201,6 +8798,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-typed": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||||
@@ -8700,6 +9303,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimalistic-assert": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -8989,6 +9604,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-url": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-z9nC87iaZXXySbWWtTHfCFJyFvKaUAW6lODhikG7ILSbVgmwuFjUqkgnheHvAUcGedO29e2QGBRXMUD64aurqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/notepack.io": {
|
"node_modules/notepack.io": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz",
|
||||||
@@ -9452,6 +10079,15 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkce-challenge": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -9687,6 +10323,66 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.7.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@@ -9852,6 +10548,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "2.0.0-next.5",
|
"version": "2.0.0-next.5",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||||
@@ -10044,6 +10749,32 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router/node_modules/path-to-regexp": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rsa-pem-from-mod-exp": {
|
"node_modules/rsa-pem-from-mod-exp": {
|
||||||
"version": "0.8.6",
|
"version": "0.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.6.tgz",
|
||||||
@@ -12310,6 +13041,24 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod-to-json-schema": {
|
||||||
|
"version": "3.25.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
||||||
|
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25 || ^4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||||
"@aws-sdk/lib-storage": "^3.1020.0",
|
"@aws-sdk/lib-storage": "^3.1020.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1020.0",
|
"@aws-sdk/s3-request-presigner": "^3.1020.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",
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
"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",
|
||||||
|
"normalize-url": "^9.0.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -98,12 +98,26 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
|||||||
socket.JobData.ownr_fn || ""
|
socket.JobData.ownr_fn || ""
|
||||||
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
|
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
|
||||||
);
|
);
|
||||||
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
|
|
||||||
socket.ownerRef = ownerRef;
|
|
||||||
|
|
||||||
WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
//If this is an AR customer, don't do anything.
|
||||||
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
|
|
||||||
socket.vehicleRef = vehicleRef;
|
const selectedCustomer = [...(socket.DMSVehCustomer ? [{ ...socket.DMSVehCustomer, vinOwner: true }] : []),
|
||||||
|
...socket.DMSCustList]?.find((cust) => cust.ContactId === selectedCustomerId);
|
||||||
|
|
||||||
|
if (selectedCustomer?.IsARCustomer) {
|
||||||
|
|
||||||
|
WsLogger.createLogEvent(socket, "INFO", `Skipping contact and vehicle update becuase it is marked as an AR contact in PBS.`);
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
|
||||||
|
socket.ownerRef = ownerRef;
|
||||||
|
WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
||||||
|
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
|
||||||
|
socket.vehicleRef = vehicleRef;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
WsLogger.createLogEvent(
|
WsLogger.createLogEvent(
|
||||||
socket,
|
socket,
|
||||||
|
|||||||
@@ -1,73 +1,71 @@
|
|||||||
const { isString } = require("lodash");
|
const { isString } = require("lodash");
|
||||||
const { sendServerEmail } = require("../email/sendemail");
|
const { sendServerEmail } = require("../email/sendemail");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { raw } = require("express");
|
|
||||||
|
|
||||||
const SUPPORT_EMAIL = "patrick@imexsystems.ca";
|
const SUPPORT_EMAIL = "support@imexsystems.ca";
|
||||||
|
|
||||||
const safeJsonParse = (maybeJson) => {
|
const safeJsonParse = (maybeJson) => {
|
||||||
if (!isString(maybeJson)) return null;
|
if (!isString(maybeJson)) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(maybeJson);
|
return JSON.parse(maybeJson);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleBillAiFeedback = async (req, res) => {
|
const handleBillAiFeedback = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rating = req.body?.rating;
|
const rating = req.body?.rating;
|
||||||
const comments = isString(req.body?.comments) ? req.body?.comments?.trim() : "";
|
const comments = isString(req.body?.comments) ? req.body?.comments?.trim() : "";
|
||||||
|
|
||||||
const billFormValues = safeJsonParse(req.body?.billFormValues);
|
const billFormValues = safeJsonParse(req.body?.billFormValues);
|
||||||
const rawAIData = safeJsonParse(req.body?.rawAIData);
|
const rawAIData = safeJsonParse(req.body?.rawAIData);
|
||||||
|
|
||||||
const jobid = billFormValues?.jobid || billFormValues?.jobId || "unknown";
|
const jobid = billFormValues?.jobid || billFormValues?.jobId || "unknown";
|
||||||
const shopname = req.body?.shopname || "unknown";
|
const shopname = req.body?.shopname || "unknown";
|
||||||
const subject = `Bill AI Feedback (${rating === "up" ? "+" : "-"}) Shop=${shopname} jobid=${jobid}`;
|
const subject = `Bill AI Feedback (${rating === "up" ? "+" : "-"}) Shop=${shopname} jobid=${jobid}`;
|
||||||
|
|
||||||
const text = [
|
const text = [
|
||||||
`User: ${req?.user?.email || "unknown"}`,
|
`User: ${req?.user?.email || "unknown"}`,
|
||||||
`Rating: ${rating}`,
|
`Rating: ${rating}`,
|
||||||
comments ? `Comments: ${comments}` : "Comments: (none)",
|
comments ? `Comments: ${comments}` : "Comments: (none)",
|
||||||
"",
|
"",
|
||||||
"Form Values (User):",
|
"Form Values (User):",
|
||||||
JSON.stringify(billFormValues, null, 4),
|
JSON.stringify(billFormValues, null, 4),
|
||||||
"",
|
"",
|
||||||
"Raw AI Data:",
|
"Raw AI Data:",
|
||||||
JSON.stringify(rawAIData, null, 4)
|
JSON.stringify(rawAIData, null, 4)
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
if (req.file?.buffer) {
|
if (req.file?.buffer) {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
filename: req.file.originalname || `bill-${jobid}.pdf`,
|
filename: req.file.originalname || `bill-${jobid}.pdf`,
|
||||||
content: req.file.buffer,
|
content: req.file.buffer,
|
||||||
contentType: req.file.mimetype || "application/pdf"
|
contentType: req.file.mimetype || "application/pdf"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await sendServerEmail({
|
|
||||||
to: [SUPPORT_EMAIL],
|
|
||||||
subject,
|
|
||||||
type: "text",
|
|
||||||
text,
|
|
||||||
attachments
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("bill-ai-feedback-error", "ERROR", req?.user?.email, null, {
|
|
||||||
message: error?.message,
|
|
||||||
stack: error?.stack
|
|
||||||
});
|
|
||||||
return res.status(500).json({ message: "Failed to submit feedback" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await sendServerEmail({
|
||||||
|
to: [SUPPORT_EMAIL],
|
||||||
|
subject,
|
||||||
|
type: "text",
|
||||||
|
text,
|
||||||
|
attachments
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("bill-ai-feedback-error", "ERROR", req?.user?.email, null, {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
return res.status(500).json({ message: "Failed to submit feedback" });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
handleBillAiFeedback
|
handleBillAiFeedback
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
const { TextractClient, StartExpenseAnalysisCommand, GetExpenseAnalysisCommand, AnalyzeExpenseCommand } = require("@aws-sdk/client-textract");
|
const {
|
||||||
|
TextractClient,
|
||||||
|
StartExpenseAnalysisCommand,
|
||||||
|
GetExpenseAnalysisCommand,
|
||||||
|
AnalyzeExpenseCommand
|
||||||
|
} = require("@aws-sdk/client-textract");
|
||||||
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
|
||||||
const { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } = require("@aws-sdk/client-sqs");
|
const { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } = require("@aws-sdk/client-sqs");
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require("uuid");
|
||||||
const { getTextractJobKey, setTextractJob, getTextractJob, getFileType, getPdfPageCount, hasActiveJobs } = require("./bill-ocr-helpers");
|
const {
|
||||||
|
getTextractJobKey,
|
||||||
|
setTextractJob,
|
||||||
|
getTextractJob,
|
||||||
|
getFileType,
|
||||||
|
getPdfPageCount,
|
||||||
|
hasActiveJobs
|
||||||
|
} = require("./bill-ocr-helpers");
|
||||||
const { extractInvoiceData, processScanData } = require("./bill-ocr-normalize");
|
const { extractInvoiceData, processScanData } = require("./bill-ocr-normalize");
|
||||||
const { generateBillFormData } = require("./bill-ocr-generator");
|
const { generateBillFormData } = require("./bill-ocr-generator");
|
||||||
const logger = require("../../utils/logger");
|
const logger = require("../../utils/logger");
|
||||||
@@ -10,11 +22,11 @@ const _ = require("lodash");
|
|||||||
|
|
||||||
// Initialize AWS clients
|
// Initialize AWS clients
|
||||||
const awsConfig = {
|
const awsConfig = {
|
||||||
region: process.env.AWS_AI_REGION || "ca-central-1",
|
region: process.env.AWS_AI_REGION || "ca-central-1",
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: process.env.AWS_AI_ACCESS_KEY_ID,
|
accessKeyId: process.env.AWS_AI_ACCESS_KEY_ID,
|
||||||
secretAccessKey: process.env.AWS_AI_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.AWS_AI_SECRET_ACCESS_KEY
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const textractClient = new TextractClient(awsConfig);
|
const textractClient = new TextractClient(awsConfig);
|
||||||
@@ -23,318 +35,339 @@ const sqsClient = new SQSClient(awsConfig);
|
|||||||
|
|
||||||
let redisPubClient = null;
|
let redisPubClient = null;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the bill-ocr module with Redis client
|
* Initialize the bill-ocr module with Redis client
|
||||||
* @param {Object} pubClient - Redis cluster client
|
* @param {Object} pubClient - Redis cluster client
|
||||||
*/
|
*/
|
||||||
function initializeBillOcr(pubClient) {
|
function initializeBillOcr(pubClient) {
|
||||||
redisPubClient = pubClient;
|
redisPubClient = pubClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if job exists by Textract job ID
|
* Check if job exists by Textract job ID
|
||||||
* @param {string} textractJobId
|
* @param {string} textractJobId
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function jobExists(textractJobId) {
|
async function jobExists(textractJobId) {
|
||||||
if (!redisPubClient) {
|
if (!redisPubClient) {
|
||||||
throw new Error('Redis client not initialized. Call initializeBillOcr first.');
|
throw new Error("Redis client not initialized. Call initializeBillOcr first.");
|
||||||
}
|
}
|
||||||
const key = getTextractJobKey(textractJobId);
|
const key = getTextractJobKey(textractJobId);
|
||||||
const exists = await redisPubClient.exists(key);
|
const exists = await redisPubClient.exists(key);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBillOcr(req, res) {
|
async function handleBillOcr(req, res) {
|
||||||
// Check if file was uploaded
|
// Check if file was uploaded
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).send({ error: 'No file uploaded.' });
|
return res.status(400).send({ error: "No file uploaded." });
|
||||||
}
|
}
|
||||||
|
|
||||||
// The uploaded file is available in request file
|
// The uploaded file is available in request file
|
||||||
const uploadedFile = req.file;
|
const uploadedFile = req.file;
|
||||||
const { jobid, bodyshopid, partsorderid } = req.body;
|
const { jobid, bodyshopid, partsorderid } = req.body;
|
||||||
logger.log("bill-ocr-start", "DEBUG", req.user.email, jobid, null);
|
logger.log("bill-ocr-start", "DEBUG", req.user.email, jobid, null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileType = getFileType(uploadedFile);
|
const fileType = getFileType(uploadedFile);
|
||||||
// Images are always processed synchronously (single page)
|
// Images are always processed synchronously (single page)
|
||||||
if (fileType === 'image') {
|
if (fileType === "image") {
|
||||||
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
||||||
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
const billForm = await generateBillFormData({
|
||||||
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ..._.omit(processedData, "originalTextractResponse"), billForm });
|
processedData: processedData,
|
||||||
|
jobid,
|
||||||
|
bodyshopid,
|
||||||
|
partsorderid,
|
||||||
|
req: req
|
||||||
|
});
|
||||||
|
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, {
|
||||||
|
..._.omit(processedData, "originalTextractResponse"),
|
||||||
|
billForm
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
status: 'COMPLETED',
|
status: "COMPLETED",
|
||||||
data: { ...processedData, billForm },
|
data: { ...processedData, billForm },
|
||||||
message: 'Invoice processing completed'
|
message: "Invoice processing completed"
|
||||||
});
|
});
|
||||||
} else if (fileType === 'pdf') {
|
} else if (fileType === "pdf") {
|
||||||
// Check the number of pages in the PDF
|
// Check the number of pages in the PDF
|
||||||
const pageCount = await getPdfPageCount(uploadedFile.buffer);
|
const pageCount = await getPdfPageCount(uploadedFile.buffer);
|
||||||
|
|
||||||
if (pageCount === 1) {
|
if (pageCount === 1) {
|
||||||
// Process synchronously for single-page documents
|
// Process synchronously for single-page documents
|
||||||
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
||||||
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
const billForm = await generateBillFormData({
|
||||||
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ..._.omit(processedData, "originalTextractResponse"), billForm });
|
processedData: processedData,
|
||||||
return res.status(200).json({
|
jobid,
|
||||||
success: true,
|
bodyshopid,
|
||||||
status: 'COMPLETED',
|
partsorderid,
|
||||||
data: { ...processedData, billForm },
|
req: req
|
||||||
message: 'Invoice processing completed'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Start the Textract job (non-blocking) for multi-page documents
|
|
||||||
const jobInfo = await startTextractJob(uploadedFile.buffer, { jobid, bodyshopid, partsorderid });
|
|
||||||
logger.log("bill-ocr-multipage-start", "DEBUG", req.user.email, jobid, jobInfo);
|
|
||||||
|
|
||||||
return res.status(202).json({
|
|
||||||
success: true,
|
|
||||||
textractJobId: jobInfo.jobId,
|
|
||||||
message: 'Invoice processing started',
|
|
||||||
statusUrl: `/ai/bill-ocr/status/${jobInfo.jobId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.log("bill-ocr-unsupported-filetype", "WARN", req.user.email, jobid, { fileType });
|
|
||||||
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Unsupported file type',
|
|
||||||
message: 'Please upload a PDF or supported image file (JPEG, PNG, TIFF)'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("bill-ocr-error", "ERROR", req.user.email, jobid, { error: error.message, stack: error.stack });
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Failed to start invoice processing',
|
|
||||||
message: error.message
|
|
||||||
});
|
});
|
||||||
|
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, {
|
||||||
|
..._.omit(processedData, "originalTextractResponse"),
|
||||||
|
billForm
|
||||||
|
});
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
status: "COMPLETED",
|
||||||
|
data: { ...processedData, billForm },
|
||||||
|
message: "Invoice processing completed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Start the Textract job (non-blocking) for multi-page documents
|
||||||
|
const jobInfo = await startTextractJob(uploadedFile.buffer, { jobid, bodyshopid, partsorderid });
|
||||||
|
logger.log("bill-ocr-multipage-start", "DEBUG", req.user.email, jobid, jobInfo);
|
||||||
|
|
||||||
|
return res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
textractJobId: jobInfo.jobId,
|
||||||
|
message: "Invoice processing started",
|
||||||
|
statusUrl: `/ai/bill-ocr/status/${jobInfo.jobId}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log("bill-ocr-unsupported-filetype", "WARN", req.user.email, jobid, { fileType });
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Unsupported file type",
|
||||||
|
message: "Please upload a PDF or supported image file (JPEG, PNG, TIFF)"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("bill-ocr-error", "ERROR", req.user.email, jobid, { error: error.message, stack: error.stack });
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Failed to start invoice processing",
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBillOcrStatus(req, res) {
|
async function handleBillOcrStatus(req, res) {
|
||||||
const { textractJobId } = req.params;
|
const { textractJobId } = req.params;
|
||||||
|
|
||||||
if (!textractJobId) {
|
if (!textractJobId) {
|
||||||
logger.log("bill-ocr-status-error", "WARN", req.user.email, null, { error: 'No textractJobId found in params' });
|
logger.log("bill-ocr-status-error", "WARN", req.user.email, null, { error: "No textractJobId found in params" });
|
||||||
return res.status(400).json({ error: 'Job ID is required' });
|
return res.status(400).json({ error: "Job ID is required" });
|
||||||
|
}
|
||||||
|
const jobStatus = await getTextractJob({ redisPubClient, textractJobId });
|
||||||
|
|
||||||
}
|
if (!jobStatus) {
|
||||||
const jobStatus = await getTextractJob({ redisPubClient, textractJobId });
|
return res.status(404).json({ error: "Job not found" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!jobStatus) {
|
if (jobStatus.status === "COMPLETED") {
|
||||||
return res.status(404).json({ error: 'Job not found' });
|
// Generate billForm on-demand if not already generated
|
||||||
}
|
let billForm = jobStatus.data?.billForm;
|
||||||
|
|
||||||
if (jobStatus.status === 'COMPLETED') {
|
if (!billForm && jobStatus.context) {
|
||||||
// Generate billForm on-demand if not already generated
|
try {
|
||||||
let billForm = jobStatus.data?.billForm;
|
billForm = await generateBillFormData({
|
||||||
|
processedData: jobStatus.data,
|
||||||
|
jobid: jobStatus.context.jobid,
|
||||||
|
bodyshopid: jobStatus.context.bodyshopid,
|
||||||
|
partsorderid: jobStatus.context.partsorderid,
|
||||||
|
req: req // Now we have request context!
|
||||||
|
});
|
||||||
|
logger.log("bill-ocr-multipage-complete", "DEBUG", req.user.email, jobStatus.context.jobid, {
|
||||||
|
...jobStatus.data,
|
||||||
|
billForm
|
||||||
|
});
|
||||||
|
|
||||||
if (!billForm && jobStatus.context) {
|
// Cache the billForm back to Redis for future requests
|
||||||
try {
|
await setTextractJob({
|
||||||
billForm = await generateBillFormData({
|
redisPubClient,
|
||||||
processedData: jobStatus.data,
|
textractJobId,
|
||||||
jobid: jobStatus.context.jobid,
|
jobData: {
|
||||||
bodyshopid: jobStatus.context.bodyshopid,
|
...jobStatus,
|
||||||
partsorderid: jobStatus.context.partsorderid,
|
|
||||||
req: req // Now we have request context!
|
|
||||||
});
|
|
||||||
logger.log("bill-ocr-multipage-complete", "DEBUG", req.user.email, jobStatus.context.jobid, { ...jobStatus.data, billForm });
|
|
||||||
|
|
||||||
// Cache the billForm back to Redis for future requests
|
|
||||||
await setTextractJob({
|
|
||||||
redisPubClient,
|
|
||||||
textractJobId,
|
|
||||||
jobData: {
|
|
||||||
...jobStatus,
|
|
||||||
data: {
|
|
||||||
...jobStatus.data,
|
|
||||||
billForm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("bill-ocr-multipage-error", "ERROR", req.user.email, jobStatus.context.jobid, { ...jobStatus.data, error: error.message, stack: error.stack });
|
|
||||||
|
|
||||||
return res.status(500).send({
|
|
||||||
status: 'COMPLETED',
|
|
||||||
error: 'Data processed but failed to generate bill form',
|
|
||||||
message: error.message,
|
|
||||||
data: jobStatus.data // Still return the raw processed data
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
status: 'COMPLETED',
|
|
||||||
data: {
|
data: {
|
||||||
...jobStatus.data,
|
...jobStatus.data,
|
||||||
billForm
|
billForm
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("bill-ocr-multipage-error", "ERROR", req.user.email, jobStatus.context.jobid, {
|
||||||
|
...jobStatus.data,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
} else if (jobStatus.status === 'FAILED') {
|
|
||||||
logger.log("bill-ocr-multipage-failed", "ERROR", req.user.email, jobStatus.context.jobid, { ...jobStatus.data, error: jobStatus.error, });
|
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).send({
|
||||||
status: 'FAILED',
|
status: "COMPLETED",
|
||||||
error: jobStatus.error
|
error: "Data processed but failed to generate bill form",
|
||||||
});
|
message: error.message,
|
||||||
} else {
|
data: jobStatus.data // Still return the raw processed data
|
||||||
return res.status(200).json({
|
|
||||||
status: jobStatus.status
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
status: "COMPLETED",
|
||||||
|
data: {
|
||||||
|
...jobStatus.data,
|
||||||
|
billForm
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (jobStatus.status === "FAILED") {
|
||||||
|
logger.log("bill-ocr-multipage-failed", "ERROR", req.user.email, jobStatus.context.jobid, {
|
||||||
|
...jobStatus.data,
|
||||||
|
error: jobStatus.error
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
status: "FAILED",
|
||||||
|
error: jobStatus.error
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(200).json({
|
||||||
|
status: jobStatus.status
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a single-page document synchronously using AnalyzeExpenseCommand
|
* Process a single-page document synchronously using AnalyzeExpenseCommand
|
||||||
* @param {Buffer} pdfBuffer
|
* @param {Buffer} pdfBuffer
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async function processSinglePageDocument(pdfBuffer) {
|
async function processSinglePageDocument(pdfBuffer) {
|
||||||
const analyzeCommand = new AnalyzeExpenseCommand({
|
const analyzeCommand = new AnalyzeExpenseCommand({
|
||||||
Document: {
|
Document: {
|
||||||
Bytes: pdfBuffer
|
Bytes: pdfBuffer
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await textractClient.send(analyzeCommand);
|
const result = await textractClient.send(analyzeCommand);
|
||||||
const invoiceData = extractInvoiceData(result);
|
const invoiceData = extractInvoiceData(result);
|
||||||
const processedData = processScanData(invoiceData);
|
const processedData = processScanData(invoiceData);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...processedData,
|
...processedData
|
||||||
//Removed as this is a large object that provides minimal value to send to client.
|
//Removed as this is a large object that provides minimal value to send to client.
|
||||||
// originalTextractResponse: result
|
// originalTextractResponse: result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startTextractJob(pdfBuffer, context = {}) {
|
async function startTextractJob(pdfBuffer, context = {}) {
|
||||||
// Upload PDF to S3 temporarily for Textract async processing
|
// Upload PDF to S3 temporarily for Textract async processing
|
||||||
const { bodyshopid, jobid } = context;
|
const { bodyshopid, jobid } = context;
|
||||||
const s3Bucket = process.env.AWS_AI_BUCKET;
|
const s3Bucket = process.env.AWS_AI_BUCKET;
|
||||||
const snsTopicArn = process.env.AWS_TEXTRACT_SNS_TOPIC_ARN;
|
const snsTopicArn = process.env.AWS_TEXTRACT_SNS_TOPIC_ARN;
|
||||||
const snsRoleArn = process.env.AWS_TEXTRACT_SNS_ROLE_ARN;
|
const snsRoleArn = process.env.AWS_TEXTRACT_SNS_ROLE_ARN;
|
||||||
|
|
||||||
if (!s3Bucket) {
|
if (!s3Bucket) {
|
||||||
throw new Error('AWS_AI_BUCKET environment variable is required');
|
throw new Error("AWS_AI_BUCKET environment variable is required");
|
||||||
}
|
}
|
||||||
if (!snsTopicArn) {
|
if (!snsTopicArn) {
|
||||||
throw new Error('AWS_TEXTRACT_SNS_TOPIC_ARN environment variable is required');
|
throw new Error("AWS_TEXTRACT_SNS_TOPIC_ARN environment variable is required");
|
||||||
}
|
}
|
||||||
if (!snsRoleArn) {
|
if (!snsRoleArn) {
|
||||||
throw new Error('AWS_TEXTRACT_SNS_ROLE_ARN environment variable is required');
|
throw new Error("AWS_TEXTRACT_SNS_ROLE_ARN environment variable is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadId = uuidv4();
|
const uploadId = uuidv4();
|
||||||
const s3Key = `textract-temp/${bodyshopid}/${jobid}/${uploadId}.pdf`; //TODO Update Keys structure to something better.
|
const s3Key = `textract-temp/${bodyshopid}/${jobid}/${uploadId}.pdf`; //TODO Update Keys structure to something better.
|
||||||
|
|
||||||
// Upload to S3
|
// Upload to S3
|
||||||
const uploadCommand = new PutObjectCommand({
|
const uploadCommand = new PutObjectCommand({
|
||||||
|
Bucket: s3Bucket,
|
||||||
|
Key: s3Key,
|
||||||
|
Body: pdfBuffer,
|
||||||
|
ContentType: "application/pdf" //Hard coded - we only support PDFs for multi-page
|
||||||
|
});
|
||||||
|
await s3Client.send(uploadCommand);
|
||||||
|
|
||||||
|
// Start async Textract expense analysis with SNS notification
|
||||||
|
const startCommand = new StartExpenseAnalysisCommand({
|
||||||
|
DocumentLocation: {
|
||||||
|
S3Object: {
|
||||||
Bucket: s3Bucket,
|
Bucket: s3Bucket,
|
||||||
Key: s3Key,
|
Name: s3Key
|
||||||
Body: pdfBuffer,
|
}
|
||||||
ContentType: 'application/pdf' //Hard coded - we only support PDFs for multi-page
|
},
|
||||||
});
|
NotificationChannel: {
|
||||||
await s3Client.send(uploadCommand);
|
SNSTopicArn: snsTopicArn,
|
||||||
|
RoleArn: snsRoleArn
|
||||||
|
},
|
||||||
|
ClientRequestToken: uploadId
|
||||||
|
});
|
||||||
|
|
||||||
// Start async Textract expense analysis with SNS notification
|
const startResult = await textractClient.send(startCommand);
|
||||||
const startCommand = new StartExpenseAnalysisCommand({
|
const textractJobId = startResult.JobId;
|
||||||
DocumentLocation: {
|
|
||||||
S3Object: {
|
|
||||||
Bucket: s3Bucket,
|
|
||||||
Name: s3Key
|
|
||||||
}
|
|
||||||
},
|
|
||||||
NotificationChannel: {
|
|
||||||
SNSTopicArn: snsTopicArn,
|
|
||||||
RoleArn: snsRoleArn
|
|
||||||
},
|
|
||||||
ClientRequestToken: uploadId
|
|
||||||
});
|
|
||||||
|
|
||||||
const startResult = await textractClient.send(startCommand);
|
// Store job info in Redis using textractJobId as the key
|
||||||
const textractJobId = startResult.JobId;
|
await setTextractJob({
|
||||||
|
redisPubClient,
|
||||||
|
textractJobId,
|
||||||
|
jobData: {
|
||||||
|
status: "IN_PROGRESS",
|
||||||
|
s3Key: s3Key,
|
||||||
|
uploadId: uploadId,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
context: context // Store the context for later use
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Store job info in Redis using textractJobId as the key
|
return {
|
||||||
await setTextractJob(
|
jobId: textractJobId
|
||||||
{
|
};
|
||||||
redisPubClient,
|
|
||||||
textractJobId,
|
|
||||||
jobData: {
|
|
||||||
status: 'IN_PROGRESS',
|
|
||||||
s3Key: s3Key,
|
|
||||||
uploadId: uploadId,
|
|
||||||
startedAt: new Date().toISOString(),
|
|
||||||
context: context // Store the context for later use
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobId: textractJobId
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process SQS messages from Textract completion notifications
|
// Process SQS messages from Textract completion notifications
|
||||||
async function processSQSMessages() {
|
async function processSQSMessages() {
|
||||||
const queueUrl = process.env.AWS_TEXTRACT_SQS_QUEUE_URL;
|
const queueUrl = process.env.AWS_TEXTRACT_SQS_QUEUE_URL;
|
||||||
|
|
||||||
if (!queueUrl) {
|
// Only poll if there are active mutli page jobs in progress
|
||||||
logger.log("bill-ocr-error", "ERROR", "api", null, { message: "AWS_TEXTRACT_SQS_QUEUE_URL not configured" });
|
const hasActive = await hasActiveJobs({ redisPubClient });
|
||||||
return;
|
if (!hasActive) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only poll if there are active mutli page jobs in progress
|
try {
|
||||||
const hasActive = await hasActiveJobs({ redisPubClient });
|
const receiveCommand = new ReceiveMessageCommand({
|
||||||
if (!hasActive) {
|
QueueUrl: queueUrl,
|
||||||
return;
|
MaxNumberOfMessages: 10,
|
||||||
}
|
WaitTimeSeconds: 20,
|
||||||
|
MessageAttributeNames: ["All"]
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const result = await sqsClient.send(receiveCommand);
|
||||||
const receiveCommand = new ReceiveMessageCommand({
|
|
||||||
QueueUrl: queueUrl,
|
|
||||||
MaxNumberOfMessages: 10,
|
|
||||||
WaitTimeSeconds: 20,
|
|
||||||
MessageAttributeNames: ['All']
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await sqsClient.send(receiveCommand);
|
if (result.Messages && result.Messages.length > 0) {
|
||||||
|
logger.log("bill-ocr-sqs-processing", "DEBUG", "api", null, {
|
||||||
|
message: `Processing ${result.Messages.length} messages from SQS`
|
||||||
|
});
|
||||||
|
for (const message of result.Messages) {
|
||||||
|
try {
|
||||||
|
// Environment-level filtering: check if this message belongs to this environment
|
||||||
|
const shouldProcess = await shouldProcessMessage(message);
|
||||||
|
|
||||||
if (result.Messages && result.Messages.length > 0) {
|
if (shouldProcess) {
|
||||||
logger.log("bill-ocr-sqs-processing", "DEBUG", "api", null, { message: `Processing ${result.Messages.length} messages from SQS` });
|
await handleTextractNotification(message);
|
||||||
for (const message of result.Messages) {
|
// Delete message after successful processing
|
||||||
try {
|
const deleteCommand = new DeleteMessageCommand({
|
||||||
// Environment-level filtering: check if this message belongs to this environment
|
QueueUrl: queueUrl,
|
||||||
const shouldProcess = await shouldProcessMessage(message);
|
ReceiptHandle: message.ReceiptHandle
|
||||||
|
});
|
||||||
if (shouldProcess) {
|
await sqsClient.send(deleteCommand);
|
||||||
await handleTextractNotification(message);
|
}
|
||||||
// Delete message after successful processing
|
} catch (error) {
|
||||||
const deleteCommand = new DeleteMessageCommand({
|
logger.log("bill-ocr-sqs-processing-error", "ERROR", "api", null, {
|
||||||
QueueUrl: queueUrl,
|
message,
|
||||||
ReceiptHandle: message.ReceiptHandle
|
error: error.message,
|
||||||
});
|
stack: error.stack
|
||||||
await sqsClient.send(deleteCommand);
|
});
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
logger.log("bill-ocr-sqs-processing-error", "ERROR", "api", null, { message, error: error.message, stack: error.stack });
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
logger.log("bill-ocr-sqs-receiving-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("bill-ocr-sqs-receiving-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -343,125 +376,140 @@ async function processSQSMessages() {
|
|||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function shouldProcessMessage(message) {
|
async function shouldProcessMessage(message) {
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(message.Body);
|
const body = JSON.parse(message.Body);
|
||||||
const snsMessage = JSON.parse(body.Message);
|
const snsMessage = JSON.parse(body.Message);
|
||||||
const textractJobId = snsMessage.JobId;
|
const textractJobId = snsMessage.JobId;
|
||||||
|
|
||||||
// Check if job exists in Redis for this environment (using environment-specific prefix)
|
// Check if job exists in Redis for this environment (using environment-specific prefix)
|
||||||
const exists = await jobExists(textractJobId);
|
const exists = await jobExists(textractJobId);
|
||||||
return exists;
|
return exists;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("bill-ocr-message-check-error", "DEBUG", "api", null, { message: "Error checking if message should be processed", error: error.message, stack: error.stack });
|
logger.log("bill-ocr-message-check-error", "DEBUG", "api", null, {
|
||||||
// If we can't parse the message, don't process it
|
message: "Error checking if message should be processed",
|
||||||
return false;
|
error: error.message,
|
||||||
}
|
stack: error.stack
|
||||||
|
});
|
||||||
|
// If we can't parse the message, don't process it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTextractNotification(message) {
|
async function handleTextractNotification(message) {
|
||||||
const body = JSON.parse(message.Body);
|
const body = JSON.parse(message.Body);
|
||||||
let snsMessage
|
let snsMessage;
|
||||||
try {
|
try {
|
||||||
snsMessage = JSON.parse(body.Message);
|
snsMessage = JSON.parse(body.Message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("bill-ocr-handle-textract-error", "DEBUG", "api", null, { message: "Error parsing SNS message - invalid message format.", error: error.message, stack: error.stack, body });
|
logger.log("bill-ocr-handle-textract-error", "DEBUG", "api", null, {
|
||||||
return;
|
message: "Error parsing SNS message - invalid message format.",
|
||||||
}
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const textractJobId = snsMessage.JobId;
|
const textractJobId = snsMessage.JobId;
|
||||||
const status = snsMessage.Status;
|
const status = snsMessage.Status;
|
||||||
|
|
||||||
// Get job info from Redis
|
// Get job info from Redis
|
||||||
const jobInfo = await getTextractJob({ redisPubClient, textractJobId });
|
const jobInfo = await getTextractJob({ redisPubClient, textractJobId });
|
||||||
|
|
||||||
if (!jobInfo) {
|
if (!jobInfo) {
|
||||||
logger.log("bill-ocr-job-not-found", "DEBUG", "api", null, { message: `Job info not found in Redis for Textract job ID: ${textractJobId}`, textractJobId, snsMessage });
|
logger.log("bill-ocr-job-not-found", "DEBUG", "api", null, {
|
||||||
return;
|
message: `Job info not found in Redis for Textract job ID: ${textractJobId}`,
|
||||||
}
|
textractJobId,
|
||||||
|
snsMessage
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (status === 'SUCCEEDED') {
|
if (status === "SUCCEEDED") {
|
||||||
// Retrieve the results
|
// Retrieve the results
|
||||||
const { processedData, originalResponse } = await retrieveTextractResults(textractJobId);
|
const { processedData, originalResponse } = await retrieveTextractResults(textractJobId);
|
||||||
|
|
||||||
// Store the processed data - billForm will be generated on-demand in the status endpoint
|
// Store the processed data - billForm will be generated on-demand in the status endpoint
|
||||||
await setTextractJob(
|
await setTextractJob({
|
||||||
{
|
redisPubClient,
|
||||||
redisPubClient,
|
textractJobId,
|
||||||
textractJobId,
|
jobData: {
|
||||||
jobData: {
|
...jobInfo,
|
||||||
...jobInfo,
|
status: "COMPLETED",
|
||||||
status: 'COMPLETED',
|
data: {
|
||||||
data: {
|
...processedData
|
||||||
...processedData,
|
//Removed as this is a large object that provides minimal value to send to client.
|
||||||
//Removed as this is a large object that provides minimal value to send to client.
|
// originalTextractResponse: originalResponse
|
||||||
// originalTextractResponse: originalResponse
|
},
|
||||||
},
|
completedAt: new Date().toISOString()
|
||||||
completedAt: new Date().toISOString()
|
}
|
||||||
}
|
});
|
||||||
}
|
} else if (status === "FAILED") {
|
||||||
);
|
await setTextractJob({
|
||||||
} else if (status === 'FAILED') {
|
redisPubClient,
|
||||||
await setTextractJob(
|
textractJobId,
|
||||||
{
|
jobData: {
|
||||||
redisPubClient,
|
...jobInfo,
|
||||||
textractJobId,
|
status: "FAILED",
|
||||||
jobData: {
|
error: snsMessage.StatusMessage || "Textract job failed",
|
||||||
...jobInfo,
|
completedAt: new Date().toISOString()
|
||||||
status: 'FAILED',
|
}
|
||||||
error: snsMessage.StatusMessage || 'Textract job failed',
|
});
|
||||||
completedAt: new Date().toISOString()
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retrieveTextractResults(textractJobId) {
|
async function retrieveTextractResults(textractJobId) {
|
||||||
// Handle pagination if there are multiple pages of results
|
// Handle pagination if there are multiple pages of results
|
||||||
let allExpenseDocuments = [];
|
let allExpenseDocuments = [];
|
||||||
let nextToken = null;
|
let nextToken = null;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const getCommand = new GetExpenseAnalysisCommand({
|
const getCommand = new GetExpenseAnalysisCommand({
|
||||||
JobId: textractJobId,
|
JobId: textractJobId,
|
||||||
NextToken: nextToken
|
NextToken: nextToken
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await textractClient.send(getCommand);
|
const result = await textractClient.send(getCommand);
|
||||||
|
|
||||||
if (result.ExpenseDocuments) {
|
if (result.ExpenseDocuments) {
|
||||||
allExpenseDocuments = allExpenseDocuments.concat(result.ExpenseDocuments);
|
allExpenseDocuments = allExpenseDocuments.concat(result.ExpenseDocuments);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextToken = result.NextToken;
|
nextToken = result.NextToken;
|
||||||
} while (nextToken);
|
} while (nextToken);
|
||||||
|
|
||||||
// Store the complete original response
|
// Store the complete original response
|
||||||
const fullTextractResponse = { ExpenseDocuments: allExpenseDocuments };
|
const fullTextractResponse = { ExpenseDocuments: allExpenseDocuments };
|
||||||
|
|
||||||
// Extract invoice data from Textract response
|
// Extract invoice data from Textract response
|
||||||
const invoiceData = extractInvoiceData(fullTextractResponse);
|
const invoiceData = extractInvoiceData(fullTextractResponse);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
processedData: processScanData(invoiceData),
|
processedData: processScanData(invoiceData),
|
||||||
originalResponse: fullTextractResponse
|
originalResponse: fullTextractResponse
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start SQS polling (call this when server starts)
|
// Start SQS polling (call this when server starts)
|
||||||
function startSQSPolling() {
|
function startSQSPolling() {
|
||||||
const pollInterval = setInterval(() => {
|
const queueUrl = process.env.AWS_TEXTRACT_SQS_QUEUE_URL;
|
||||||
processSQSMessages().catch(error => {
|
|
||||||
logger.log("bill-ocr-sqs-poll-error", "ERROR", "api", null, { message: error.message, stack: error.stack });
|
if (!queueUrl) {
|
||||||
});
|
logger.log("bill-ocr-error", "ERROR", "api", null, { message: "AWS_TEXTRACT_SQS_QUEUE_URL not configured" });
|
||||||
}, 10000); // Poll every 10 seconds
|
return;
|
||||||
return pollInterval;
|
}
|
||||||
|
|
||||||
|
const pollInterval = setInterval(() => {
|
||||||
|
processSQSMessages().catch((error) => {
|
||||||
|
logger.log("bill-ocr-sqs-poll-error", "ERROR", "api", null, { message: error.message, stack: error.stack });
|
||||||
|
});
|
||||||
|
}, 10000); // Poll every 10 seconds
|
||||||
|
return pollInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initializeBillOcr,
|
initializeBillOcr,
|
||||||
handleBillOcr,
|
handleBillOcr,
|
||||||
handleBillOcrStatus,
|
handleBillOcrStatus,
|
||||||
startSQSPolling
|
startSQSPolling
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ exports.default = async (req, res) => {
|
|||||||
emailer
|
emailer
|
||||||
.sendTaskEmail({
|
.sendTaskEmail({
|
||||||
to: [
|
to: [
|
||||||
"patrick.fic@convenient-brands.com",
|
|
||||||
"bradley.rhoades@convenient-brands.com",
|
"bradley.rhoades@convenient-brands.com",
|
||||||
"jrome@rometech.com",
|
"jrome@rometech.com",
|
||||||
"ivana@imexsystems.ca",
|
"ivana@imexsystems.ca",
|
||||||
|
|||||||
@@ -422,7 +422,6 @@ const emailBounce = async (req, res) => {
|
|||||||
rome: `Rome Online <noreply@romeonline.io>`
|
rome: `Rome Online <noreply@romeonline.io>`
|
||||||
}),
|
}),
|
||||||
to: replyTo,
|
to: replyTo,
|
||||||
//bcc: "patrick@snapt.ca",
|
|
||||||
subject: `${InstanceManager({
|
subject: `${InstanceManager({
|
||||||
imex: "ImEX Online",
|
imex: "ImEX Online",
|
||||||
rome: "Rome Online"
|
rome: "Rome Online"
|
||||||
|
|||||||
488
server/esign/esign-new.js
Normal file
488
server/esign/esign-new.js
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
|
||||||
|
const { Documenso } = require("@documenso/sdk-typescript");
|
||||||
|
const axios = require("axios");
|
||||||
|
const { jsrAuthString } = require("../utils/utils");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
//Need to pull the key dynamically to send documents.
|
||||||
|
const JSR_SERVER = process.env.JSR_URL || "https://reports.imex.online";
|
||||||
|
const jsreport = require("@jsreport/nodejs-client");
|
||||||
|
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIGNATURE_DOCUMENT, DISTRIBUTE_ESIGNATURE_DOCUMENT, QUERY_ESIGNATURE_BY_EXTERNAL_ID, UPDATE_ESIGNATURE_DOCUMENT, QUERY_DOCUMENSO_KEY, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
function parseJsonField(value, fallback = null) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultEsignData({ esigData, bodyshop, fileName }) {
|
||||||
|
const fallbackTitle = fileName || `Esign request from ${bodyshop.shopname}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...esigData,
|
||||||
|
title: esigData?.title || fallbackTitle,
|
||||||
|
subject: esigData?.subject || `Esign request from ${bodyshop.shopname}`,
|
||||||
|
message: esigData?.message || `Please review and sign the document from ${bodyshop.shopname}.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClientError(message, statusCode = 400) {
|
||||||
|
const error = new Error(message);
|
||||||
|
error.statusCode = statusCode;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEmail(email) {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJobOwnerName(jobData, email) {
|
||||||
|
const ownerName = [jobData?.ownr_fn, jobData?.ownr_ln].filter(Boolean).join(" ").trim();
|
||||||
|
return ownerName || jobData?.ownr_co_nm || email;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJobOwnerRecipients(jobData) {
|
||||||
|
const ownerEmail = jobData?.ownr_ea?.trim();
|
||||||
|
|
||||||
|
if (!ownerEmail) {
|
||||||
|
throw createClientError("Job owner email is required before sending an e-signature request.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(ownerEmail)) {
|
||||||
|
throw createClientError(`Job owner email "${ownerEmail}" is not valid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
email: ownerEmail,
|
||||||
|
name: getJobOwnerName(jobData, ownerEmail),
|
||||||
|
role: "SIGNER"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDocumensoClient({ bodyshopid, req }) {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
const { bodyshops_by_pk: { documenso_api_key } } = await client.request(QUERY_DOCUMENSO_KEY, { bodyshopid });
|
||||||
|
return new Documenso({
|
||||||
|
apiKey: documenso_api_key,//Done on a by team basis,
|
||||||
|
serverURL: "https://sign.imex.online/api/v2",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createEsignDocumentFromPdf({ req, bodyshop, pdfBuffer, esigData, fileName }) {
|
||||||
|
const resolvedEsigData = getDefaultEsignData({ esigData, bodyshop, fileName });
|
||||||
|
const fileBlob = new Blob([pdfBuffer], { type: "application/pdf" });
|
||||||
|
const jobid = req.body.jobid;
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid });
|
||||||
|
const recipients = getJobOwnerRecipients(jobData);
|
||||||
|
|
||||||
|
const documenso = await getDocumensoClient({ bodyshopid: bodyshop.id, req })
|
||||||
|
|
||||||
|
const createDocumentResponse = await documenso.documents.create({
|
||||||
|
payload: {
|
||||||
|
title: resolvedEsigData.title,
|
||||||
|
externalId: `${jobid}|${req.user?.email}`,
|
||||||
|
recipients,
|
||||||
|
meta: {
|
||||||
|
timezone: bodyshop.timezone,
|
||||||
|
dateFormat: "MM/dd/yyyy hh:mm a",
|
||||||
|
language: "en",
|
||||||
|
subject: resolvedEsigData.subject,
|
||||||
|
message: resolvedEsigData.message,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
file: fileBlob
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentResult = await documenso.documents.get({
|
||||||
|
documentId: createDocumentResponse.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolvedEsigData?.fields && resolvedEsigData.fields.length > 0) {
|
||||||
|
try {
|
||||||
|
await documenso.envelopes.fields.createMany({
|
||||||
|
envelopeId: createDocumentResponse.envelopeId,
|
||||||
|
data: resolvedEsigData.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({});
|
||||||
|
|
||||||
|
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
|
||||||
|
audit: {
|
||||||
|
jobid,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
operation: `Esignature document created. Subject: ${resolvedEsigData.subject || "No subject"}, Message: ${resolvedEsigData.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
|
||||||
|
useremail: req.user?.email,
|
||||||
|
type: 'esig-create'
|
||||||
|
},
|
||||||
|
esig: {
|
||||||
|
jobid,
|
||||||
|
external_document_id: createDocumentResponse.id.toString(),
|
||||||
|
subject: resolvedEsigData.subject || "No subject",
|
||||||
|
message: resolvedEsigData.message || "No message",
|
||||||
|
title: resolvedEsigData.title || "No title",
|
||||||
|
status: "DRAFT",
|
||||||
|
recipients: recipients,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: presignToken.token,
|
||||||
|
documentId: createDocumentResponse.id,
|
||||||
|
envelopeId: createDocumentResponse.envelopeId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function distributeDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
const { documentId, bodyshopid } = req.body;
|
||||||
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
||||||
|
|
||||||
|
const distributeResult = await documenso.documents.distribute({
|
||||||
|
documentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.request(DISTRIBUTE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: documentId.toString(),
|
||||||
|
esig_update: {
|
||||||
|
status: "SENT"
|
||||||
|
},
|
||||||
|
audit: {
|
||||||
|
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) {
|
||||||
|
|
||||||
|
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.", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function redistributeDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
const { documentId, bodyshopid } = req.body;
|
||||||
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
||||||
|
|
||||||
|
|
||||||
|
const document = await documenso.documents.get({
|
||||||
|
documentId: parseInt(documentId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const distributeResult = await documenso.documents.redistribute({
|
||||||
|
documentId: parseInt(documentId),
|
||||||
|
recipients: document.recipients.filter(r => r.signingStatus === "NOT_SIGNED").map(r => r.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
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}) redistributed to recipients.`,
|
||||||
|
useremail: req.user?.email,
|
||||||
|
type: 'esig-redistribute'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ success: true, distributeResult });
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
logger.log(`esig-redistribute-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "An error occurred while redistributing the document.", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
const { documentId, bodyshopid } = req.body;
|
||||||
|
const { esignature_documents } = await client.request(QUERY_ESIGNATURE_BY_EXTERNAL_ID, { external_document_id: documentId.toString() });
|
||||||
|
|
||||||
|
if (!esignature_documents || esignature_documents.length === 0) {
|
||||||
|
//return res.status(404).json({ error: "Document not found" });
|
||||||
|
}
|
||||||
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
||||||
|
|
||||||
|
const deleteResult = await documenso.documents.delete({
|
||||||
|
documentId: (documentId)
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: documentId.toString(),
|
||||||
|
esig_update: {
|
||||||
|
status: "DELETED"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.json({ success: true, deleteResult });
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
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 viewDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const { documentId, bodyshopid } = req.body;
|
||||||
|
const documenso = await getDocumensoClient({ bodyshopid, req })
|
||||||
|
|
||||||
|
const document = await documenso.document.documentDownload({
|
||||||
|
documentId: parseInt(documentId)
|
||||||
|
});
|
||||||
|
res.json({ success: true, document });
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
logger.log(`esig-view-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: "An error occurred while retrieving the document.", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newEsignDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
const { bodyshop } = req.body;
|
||||||
|
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req });
|
||||||
|
const result = await createEsignDocumentFromPdf({
|
||||||
|
req,
|
||||||
|
bodyshop,
|
||||||
|
pdfBuffer: fileBuffer,
|
||||||
|
esigData
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: _.omit(req.body, ["bodyshop"]) // bodyshop can be large, so we omit it from the logs
|
||||||
|
});
|
||||||
|
res.status(error.statusCode || 500).json({ error: "An error occurred while creating the e-sign document.", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function newCustomEsignDocument(req, res) {
|
||||||
|
try {
|
||||||
|
const bodyshop = parseJsonField(req.body.bodyshop, req.body.bodyshop);
|
||||||
|
const esigData = parseJsonField(req.body.esigData, {});
|
||||||
|
const uploadedDocument = req.file;
|
||||||
|
|
||||||
|
if (!uploadedDocument?.buffer) {
|
||||||
|
return res.status(400).json({ error: "A PDF document is required." });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadedDocument.mimetype !== "application/pdf") {
|
||||||
|
return res.status(400).json({ error: "Only PDF documents can be used for e-signature." });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.bodyshop = bodyshop;
|
||||||
|
|
||||||
|
const fileName = uploadedDocument.originalname?.replace(/\.[^.]+$/, "") || undefined;
|
||||||
|
const result = await createEsignDocumentFromPdf({
|
||||||
|
req,
|
||||||
|
bodyshop,
|
||||||
|
pdfBuffer: uploadedDocument.buffer,
|
||||||
|
esigData,
|
||||||
|
fileName
|
||||||
|
});
|
||||||
|
res.json(result);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
logger.log(`esig-new-custom-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
body: _.omit(req.body, ["bodyshop"]) // bodyshop can be large, so we omit it from the logs
|
||||||
|
});
|
||||||
|
res.status(error.statusCode || 500).json({ error: "An error occurred while creating the custom e-sign document.", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function RenderTemplate({ req }) {
|
||||||
|
const jsrAuth = jsrAuthString()
|
||||||
|
const jsreportClient = new jsreport(JSR_SERVER, 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,
|
||||||
|
esignature: true,
|
||||||
|
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) {
|
||||||
|
logger.log(`esig-data-parse-error`, "ERROR", "esig", "api", {
|
||||||
|
message: error.message, stack: error.stack,
|
||||||
|
esigData,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
parsedEsigData = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextData,
|
||||||
|
useShopSpecificTemplate,
|
||||||
|
shopSpecificFolder,
|
||||||
|
esigData: parsedEsigData
|
||||||
|
};
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
newEsignDocument,
|
||||||
|
newCustomEsignDocument,
|
||||||
|
distributeDocument,
|
||||||
|
redistributeDocument,
|
||||||
|
deleteDocument,
|
||||||
|
viewDocument,
|
||||||
|
getDocumensoClient
|
||||||
|
}
|
||||||
337
server/esign/webhook.js
Normal file
337
server/esign/webhook.js
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
const { Documenso } = require("@documenso/sdk-typescript");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const {
|
||||||
|
QUERY_META_FOR_ESIG_COMPLETION,
|
||||||
|
INSERT_ESIGNATURE_COMPLETED_DOCOUMENT,
|
||||||
|
UPDATE_ESIGNATURE_DOCUMENT,
|
||||||
|
DISTRIBUTE_ESIGNATURE_DOCUMENT,
|
||||||
|
GET_DOCUMENSO_KEY_BY_JOBID
|
||||||
|
} = require("../graphql-client/queries");
|
||||||
|
const replaceAccents = require("../utils/replaceAccents");
|
||||||
|
const { uploadFileBuffer } = require("../media/imgproxy-media");
|
||||||
|
const {
|
||||||
|
dispatchEsignDocumentOpenedNotification,
|
||||||
|
dispatchEsignDocumentCompletedNotification,
|
||||||
|
dispatchEsignDocumentUploadFailedNotification
|
||||||
|
} = require("../notifications/esignNotifications");
|
||||||
|
const axios = require("axios");
|
||||||
|
const normalizeUrl = require("normalize-url");
|
||||||
|
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration of webhook event types received from the e-signature service. These events represent different stages in
|
||||||
|
* the document signing process, such as when a document is created, sent, opened, signed, completed, rejected,
|
||||||
|
* cancelled, or when a reminder is sent. This enumeration is used to handle incoming webhook events and trigger
|
||||||
|
* appropriate actions based on the event type.
|
||||||
|
* @type {{DOCUMENT_CREATED: string, DOCUMENT_SENT: string, DOCUMENT_COMPLETED: string, DOCUMENT_REJECTED: string, DOCUMENT_CANCELLED: string, DOCUMENT_OPENED: string, DOCUMENT_SIGNED: string, DOCUMENT_REMINDER_SENT: string}}
|
||||||
|
*/
|
||||||
|
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",
|
||||||
|
DOCUMENT_REMINDER_SENT: "DOCUMENT_REMINDER_SENT"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely dispatches e-sign notifications by catching and logging any errors that occur during the dispatch process.
|
||||||
|
* This ensures that failures in notification dispatch do not affect the main flow of processing webhook events.
|
||||||
|
* The function takes an object containing the promise returned by the notification dispatch function, the event name,
|
||||||
|
* job ID, and document ID for logging purposes.
|
||||||
|
* @param param0
|
||||||
|
* @param param0.promise
|
||||||
|
* @param param0.eventName
|
||||||
|
* @param param0.jobid
|
||||||
|
* @param param0.documentId
|
||||||
|
*/
|
||||||
|
function dispatchEsignNotificationSafely({ promise, eventName, jobid, documentId }) {
|
||||||
|
promise.catch((error) => {
|
||||||
|
logger.log("esig-notification-dispatch-error", "ERROR", "notifications", "api", {
|
||||||
|
eventName,
|
||||||
|
jobid,
|
||||||
|
documentId,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming webhook events from the e-signature service. It processes different event types such as document
|
||||||
|
* opened, completed, rejected, etc., updates the document status in the database accordingly, and dispatches
|
||||||
|
* notifications to users. The function also includes error handling to log any issues that occur during processing and
|
||||||
|
* ensure a proper response is sent back to the webhook sender.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function esignWebhook(req, res) {
|
||||||
|
try {
|
||||||
|
const message = req.body;
|
||||||
|
const documentPayload = message.payload || message.payload?.payload || {};
|
||||||
|
const externalId = documentPayload.externalId || documentPayload.payload?.externalId || "";
|
||||||
|
const [jobid, uploadedBy] = externalId.split("|");
|
||||||
|
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
|
||||||
|
event: message.event,
|
||||||
|
body: message
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = (message.payload?.id || message.payload?.payload?.id)?.toString();
|
||||||
|
//TODO: Implement checks to prevent this from going backwards in status? If a request fails, it retries, which could cause a document marked as completed to be marked as rejected if the rejection event is processed after the completion event.
|
||||||
|
switch (message.event) {
|
||||||
|
case webhookTypeEnums.DOCUMENT_REMINDER_SENT:
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_OPENED:
|
||||||
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: documentId,
|
||||||
|
esig_update: {
|
||||||
|
status: "OPENED",
|
||||||
|
opened: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatchEsignNotificationSafely({
|
||||||
|
promise: dispatchEsignDocumentOpenedNotification({
|
||||||
|
jobId: jobid,
|
||||||
|
documentId,
|
||||||
|
title: documentPayload.title,
|
||||||
|
uploadedBy,
|
||||||
|
logger
|
||||||
|
}),
|
||||||
|
eventName: webhookTypeEnums.DOCUMENT_OPENED,
|
||||||
|
jobid,
|
||||||
|
documentId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_REJECTED:
|
||||||
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: documentId,
|
||||||
|
esig_update: {
|
||||||
|
status: "REJECTED",
|
||||||
|
rejected: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_CREATED:
|
||||||
|
//This is largely a throwaway event we know it was created.
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_COMPLETED:
|
||||||
|
await handleDocumentCompleted(message.payload);
|
||||||
|
break;
|
||||||
|
case webhookTypeEnums.DOCUMENT_SIGNED:
|
||||||
|
await client.request(UPDATE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: documentId,
|
||||||
|
esig_update: {
|
||||||
|
status: "SIGNED"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
res.status(200).json({ message: "Unsupported event type." });
|
||||||
|
logger.log(`esig-webhook-received-unknown`, "ERROR", "redis", "api", {
|
||||||
|
event: message.event,
|
||||||
|
body: message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.log(`esig-webhook-processed`, "INFO", "redis", "api", {
|
||||||
|
event: message.event,
|
||||||
|
documentId: message.payload?.payload?.id,
|
||||||
|
jobid: message.payload?.payload?.externalId?.split("|")[0] || null
|
||||||
|
});
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
body: req.body
|
||||||
|
});
|
||||||
|
res.status(500).json({ message: "Error processing webhook event.", error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the processing of a document completion event. This includes downloading the completed document from the
|
||||||
|
* e-signature service, uploading it to either a local media server or S3 depending on the bodyshop configuration,
|
||||||
|
* updating the document record in the database, and dispatching a notification about the document completion.
|
||||||
|
* The function also includes error handling to log any issues that occur during processing.
|
||||||
|
* @param payload
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function handleDocumentCompleted(payload) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
//Have to use globally authed cleint since this a webhook.
|
||||||
|
const {
|
||||||
|
jobs_by_pk: {
|
||||||
|
bodyshop: { documenso_api_key }
|
||||||
|
}
|
||||||
|
} = await client.request(GET_DOCUMENSO_KEY_BY_JOBID, {
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
const documenso = new Documenso({
|
||||||
|
apiKey: documenso_api_key,
|
||||||
|
serverURL: "https://sign.imex.online/api/v2"
|
||||||
|
});
|
||||||
|
|
||||||
|
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`;
|
||||||
|
const notifyUploadFailure = () =>
|
||||||
|
dispatchEsignNotificationSafely({
|
||||||
|
promise: dispatchEsignDocumentUploadFailedNotification({
|
||||||
|
jobId: jobs_by_pk.id,
|
||||||
|
documentId: payload.id.toString(),
|
||||||
|
title: payload.title,
|
||||||
|
uploadedBy: uploaded_by,
|
||||||
|
logger
|
||||||
|
}),
|
||||||
|
eventName: "DOCUMENT_UPLOAD_FAILED",
|
||||||
|
jobid,
|
||||||
|
documentId: payload.id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
|
||||||
|
const {
|
||||||
|
bodyshop: { localmediaserverhttp, localmediatoken }
|
||||||
|
} = jobs_by_pk;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
ims_token: localmediatoken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const formData = new FormData();
|
||||||
|
const fileName = document.filename?.toLowerCase().endsWith(".pdf")
|
||||||
|
? document.filename
|
||||||
|
: `${document.filename || `esignature-document-${payload.id}`}.pdf`;
|
||||||
|
const pdfBlob = new Blob([buffer], { type: "application/pdf" });
|
||||||
|
|
||||||
|
formData.append("jobid", jobid);
|
||||||
|
formData.append("file", pdfBlob, fileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imexMediaServerResponse = await axios.post(
|
||||||
|
normalizeUrl(`${localmediaserverhttp}/jobs/upload`),
|
||||||
|
formData,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imexMediaServerResponse.status === 200) {
|
||||||
|
//Succesful upload - we don't really need to do anything here.
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Local media server upload failed with status ${imexMediaServerResponse.status}: ${imexMediaServerResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-lms-upload-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid,
|
||||||
|
documentId: payload.id
|
||||||
|
});
|
||||||
|
notifyUploadFailure();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" });
|
||||||
|
|
||||||
|
if (uploadResult.success) {
|
||||||
|
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
|
||||||
|
jobid: jobid,
|
||||||
|
documentId: payload.id,
|
||||||
|
s3Key: key,
|
||||||
|
bucket: uploadResult.bucket
|
||||||
|
});
|
||||||
|
|
||||||
|
//insert the document record with the s3 key and bucket info.
|
||||||
|
await client.request(INSERT_ESIGNATURE_COMPLETED_DOCOUMENT, {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const uploadError = new Error(uploadResult.message || "S3 upload failed");
|
||||||
|
uploadError.stack = uploadResult.stack || uploadError.stack;
|
||||||
|
throw uploadError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: jobid,
|
||||||
|
documentId: payload.id
|
||||||
|
});
|
||||||
|
notifyUploadFailure();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the audit trail and records to mark the document as completed.
|
||||||
|
await client.request(DISTRIBUTE_ESIGNATURE_DOCUMENT, {
|
||||||
|
external_document_id: payload.id.toString(),
|
||||||
|
esig_update: {
|
||||||
|
status: "COMPLETED",
|
||||||
|
completed: true,
|
||||||
|
completed_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
audit: {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatchEsignNotificationSafely({
|
||||||
|
promise: dispatchEsignDocumentCompletedNotification({
|
||||||
|
jobId: jobs_by_pk.id,
|
||||||
|
documentId: payload.id.toString(),
|
||||||
|
title: payload.title,
|
||||||
|
uploadedBy: uploaded_by,
|
||||||
|
logger
|
||||||
|
}),
|
||||||
|
eventName: webhookTypeEnums.DOCUMENT_COMPLETED,
|
||||||
|
jobid,
|
||||||
|
documentId: payload.id.toString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
payload
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
esignWebhook
|
||||||
|
};
|
||||||
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;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user