Compare commits
3 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
745eb8e980 | ||
|
|
0b772133b8 | ||
|
|
318a3be786 |
3722
client/package-lock.json
generated
3722
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,60 +2,59 @@
|
||||
"name": "bodyshop",
|
||||
"version": "0.2.1",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.36.5",
|
||||
"@amplitude/analytics-browser": "^2.35.3",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^4.1.6",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@documenso/embed-react": "^0.6.0",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
||||
"@firebase/analytics": "^0.10.20",
|
||||
"@firebase/app": "^0.14.9",
|
||||
"@firebase/auth": "^1.12.1",
|
||||
"@firebase/firestore": "^4.12.0",
|
||||
"@firebase/messaging": "^0.12.24",
|
||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||
"@firebase/analytics": "^0.10.19",
|
||||
"@firebase/app": "^0.14.8",
|
||||
"@firebase/auth": "^1.12.0",
|
||||
"@firebase/firestore": "^4.11.0",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@sentry/cli": "^3.3.3",
|
||||
"@sentry/react": "^10.43.0",
|
||||
"@sentry/cli": "^3.2.2",
|
||||
"@sentry/react": "^10.40.0",
|
||||
"@sentry/vite-plugin": "^4.9.1",
|
||||
"@splitsoftware/splitio-react": "^2.6.1",
|
||||
"@tanem/react-nprogress": "^5.0.63",
|
||||
"antd": "^6.3.3",
|
||||
"antd": "^6.3.1",
|
||||
"apollo-link-logger": "^3.0.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.13.6",
|
||||
"axios": "^1.13.5",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs-business-days2": "^1.3.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"env-cmd": "^11.0.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.13.1",
|
||||
"graphql": "^16.13.0",
|
||||
"graphql-ws": "^6.0.7",
|
||||
"i18next": "^25.8.18",
|
||||
"i18next": "^25.8.13",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.40",
|
||||
"lightningcss": "^1.32.0",
|
||||
"logrocket": "^12.1.0",
|
||||
"libphonenumber-js": "^1.12.38",
|
||||
"lightningcss": "^1.31.1",
|
||||
"logrocket": "^12.0.0",
|
||||
"markerjs2": "^2.32.7",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.1.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.71",
|
||||
"posthog-js": "^1.360.2",
|
||||
"posthog-js": "^1.355.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.3.1",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -66,8 +65,8 @@
|
||||
"react-dom": "^19.2.4",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
"react-i18next": "^16.5.8",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-number-format": "^5.4.3",
|
||||
@@ -77,8 +76,8 @@
|
||||
"react-resizable": "^3.1.3",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtuoso": "^4.18.3",
|
||||
"recharts": "^3.8.0",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"recharts": "^3.7.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
@@ -86,7 +85,7 @@
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"sass": "^1.98.0",
|
||||
"sass": "^1.97.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"styled-components": "^6.3.11",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
@@ -141,16 +140,15 @@
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@dotenvx/dotenvx": "^1.55.1",
|
||||
"@dotenvx/dotenvx": "^1.52.0",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@rolldown/plugin-babel": "^0.2.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
@@ -158,18 +156,21 @@
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"globals": "^17.4.0",
|
||||
"globals": "^17.3.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"memfs": "^4.56.11",
|
||||
"memfs": "^4.56.10",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.58.2",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^8.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-babel": "^1.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^4.1.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^4.0.18",
|
||||
"workbox-window": "^7.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,10 +509,3 @@
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.esignature-embed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-width: 0;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { EmbedUpdateDocumentV1 } from "@documenso/embed-react";
|
||||
import { Modal } from "antd";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectEsignature } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
esignatureModal: selectEsignature,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("esignature"))
|
||||
});
|
||||
|
||||
export function EsignatureModalContainer({ esignatureModal, toggleModalVisible, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const { open, context } = esignatureModal;
|
||||
const { token, envelopeId, documentId, jobid } = context;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={t("jobs.labels.esignature")}
|
||||
onOk={async () => {
|
||||
try {
|
||||
const distResult = await axios.post("/esign/distribute", {
|
||||
documentId,
|
||||
envelopeId,
|
||||
jobid,
|
||||
bodyshopid: bodyshop.id
|
||||
});
|
||||
console.log("Distribution result:", distResult);
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error("Error distributing document:", error);
|
||||
}
|
||||
}}
|
||||
onCancel={async () => {
|
||||
try {
|
||||
const cancelResult = await axios.post("/esign/delete", {
|
||||
documentId,
|
||||
envelopeId
|
||||
});
|
||||
console.log("Cancel result:", cancelResult);
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error("Error cancelling document:", error);
|
||||
}
|
||||
}}
|
||||
okButtonProps={{ title: "Distribute by Email" }}
|
||||
width="90%"
|
||||
destroyOnHidden
|
||||
>
|
||||
<div style={{ height: "600px", width: "100%" }}>
|
||||
{token ? (
|
||||
<EmbedUpdateDocumentV1
|
||||
presignToken={token}
|
||||
host="https://stg-app.documenso.com"
|
||||
documentId={documentId}
|
||||
className="esignature-embed"
|
||||
onDocumentUpdated={(data) => {
|
||||
console.log("Document updated:", data.documentId);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div>No token...</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EsignatureModalContainer);
|
||||
@@ -10,6 +10,11 @@ const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const toFiniteNumber = (value) => {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
switch (type) {
|
||||
@@ -20,8 +25,15 @@ const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||
|
||||
case "text":
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
case "currency":
|
||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
||||
case "currency": {
|
||||
const numericValue = toFiniteNumber(value);
|
||||
|
||||
if (numericValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>{Dinero({ amount: Math.round(numericValue * 100) }).toFormat()}</div>;
|
||||
}
|
||||
default:
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DownOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { Space } from "antd";
|
||||
|
||||
export default function FormListMoveArrows({ move, index, total }) {
|
||||
export default function FormListMoveArrows({ move, index, total, orientation = "vertical" }) {
|
||||
const upDisabled = index === 0;
|
||||
const downDisabled = index === total - 1;
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function FormListMoveArrows({ move, index, total }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Space orientation="vertical">
|
||||
<Space orientation={orientation}>
|
||||
<UpOutlined disabled={upDisabled} onClick={handleUp} />
|
||||
<DownOutlined disabled={downDisabled} onClick={handleDown} />
|
||||
</Space>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MailOutlined, PrinterOutlined, SignatureFilled } from "@ant-design/icons";
|
||||
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
||||
import { Space, Spin } from "antd";
|
||||
import { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
@@ -10,8 +10,6 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import axios from "axios";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
printCenterModal: selectPrintCenter,
|
||||
@@ -19,25 +17,9 @@ const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEsignatureContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "esignature"
|
||||
})
|
||||
)
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function PrintCenterItemComponent({
|
||||
printCenterModal,
|
||||
setEsignatureContext,
|
||||
item,
|
||||
id,
|
||||
bodyshop,
|
||||
disabled,
|
||||
technician
|
||||
}) {
|
||||
export function PrintCenterItemComponent({ printCenterModal, item, id, bodyshop, disabled, technician }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { context } = printCenterModal;
|
||||
const notification = useNotification();
|
||||
@@ -57,30 +39,6 @@ export function PrintCenterItemComponent({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const esignatureGenerate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const {
|
||||
data: { token, documentId, evnelopeId }
|
||||
} = await axios.post("/esign/new", {
|
||||
name: item.key,
|
||||
jobid: id,
|
||||
context,
|
||||
bodyshop,
|
||||
templateObject: {
|
||||
name: item.key,
|
||||
variables: { id: id }
|
||||
}
|
||||
});
|
||||
|
||||
setEsignatureContext({ context: { token, documentId, evnelopeId, jobid: id } });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
disabled ||
|
||||
(item.featureNameRestricted && !HasFeatureAccess({ featureName: item.featureNameRestricted, bodyshop }))
|
||||
@@ -96,7 +54,6 @@ export function PrintCenterItemComponent({
|
||||
<li>
|
||||
<Space wrap>
|
||||
{item.title}
|
||||
<SignatureFilled onClick={esignatureGenerate} />
|
||||
<PrinterOutlined onClick={renderToNewWindow} />
|
||||
{!technician ? (
|
||||
<MailOutlined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import { Button, Card, Form, Input, InputNumber, Select, Space, Switch, Typography } from "antd";
|
||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch, Tag, Typography } from "antd";
|
||||
|
||||
import querystring from "query-string";
|
||||
import { useEffect } from "react";
|
||||
@@ -35,6 +35,14 @@ const PAYOUT_METHOD_OPTIONS = [
|
||||
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
||||
];
|
||||
|
||||
const TEAM_MEMBER_PRIMARY_FIELD_COLS = {
|
||||
employee: { xs: 24, lg: 13, xxl: 14 },
|
||||
allocation: { xs: 24, sm: 12, lg: 4, xxl: 4 },
|
||||
payoutMethod: { xs: 24, sm: 12, lg: 7, xxl: 6 }
|
||||
};
|
||||
|
||||
const TEAM_MEMBER_RATE_FIELD_COLS = { xs: 24, sm: 12, md: 8, lg: 6, xxl: 4 };
|
||||
|
||||
const normalizeTeamMember = (teamMember = {}) => ({
|
||||
...teamMember,
|
||||
payout_method: teamMember.payout_method || "hourly",
|
||||
@@ -52,6 +60,25 @@ const getSplitTotal = (teamMembers = []) =>
|
||||
|
||||
const hasExactSplitTotal = (teamMembers = []) => Math.abs(getSplitTotal(teamMembers) - 100) < 0.00001;
|
||||
|
||||
const getPayoutMethodTagColor = (payoutMethod) => (payoutMethod === "commission" ? "gold" : "blue");
|
||||
|
||||
const getEmployeeDisplayName = (employees = [], employeeId) => {
|
||||
const employee = employees.find((currentEmployee) => currentEmployee.id === employeeId);
|
||||
if (!employee) return null;
|
||||
|
||||
const fullName = [employee.first_name, employee.last_name].filter(Boolean).join(" ").trim();
|
||||
return fullName || employee.employee_number || null;
|
||||
};
|
||||
|
||||
const formatAllocationPercentage = (percentage) => {
|
||||
if (percentage === null || percentage === undefined || percentage === "") return null;
|
||||
|
||||
const numericValue = Number(percentage);
|
||||
if (!Number.isFinite(numericValue)) return null;
|
||||
|
||||
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
||||
};
|
||||
|
||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
@@ -80,6 +107,31 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
label: t(labelKey),
|
||||
value
|
||||
}));
|
||||
const teamName = Form.useWatch("name", form);
|
||||
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
|
||||
const teamCardTitle = teamName?.trim() || t("employee_teams.fields.name");
|
||||
|
||||
const getTeamMemberTitle = (teamMember = {}) => {
|
||||
const employeeName =
|
||||
getEmployeeDisplayName(bodyshop.employees, teamMember.employeeid) || t("employee_teams.fields.employeeid");
|
||||
const allocation = formatAllocationPercentage(teamMember.percentage);
|
||||
const payoutMethod =
|
||||
teamMember.payout_method === "commission"
|
||||
? t("employee_teams.options.commission")
|
||||
: t("employee_teams.options.hourly");
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
|
||||
<Typography.Text strong>{employeeName}</Typography.Text>
|
||||
<Tag bordered={false} color="geekblue">
|
||||
{`${t("employee_teams.fields.allocation")}: ${allocation || "--"}`}
|
||||
</Tag>
|
||||
<Tag bordered={false} color={getPayoutMethodTagColor(teamMember.payout_method)}>
|
||||
{payoutMethod}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||
const normalizedTeamMembers = employee_team_members.map((teamMember) => {
|
||||
@@ -129,7 +181,9 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
.filter((teamMember) => teamMember.id === null || teamMember.id === undefined)
|
||||
.map((teamMember) => ({ ...teamMember, teamid: search.employeeTeamId })),
|
||||
teamMemberDeletes: data.employee_teams_by_pk.employee_team_members
|
||||
.filter((teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id))
|
||||
.filter(
|
||||
(teamMember) => !normalizedTeamMembers.find((currentTeamMember) => currentTeamMember.id === teamMember.id)
|
||||
)
|
||||
.map((teamMember) => teamMember.id)
|
||||
}
|
||||
});
|
||||
@@ -172,6 +226,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={teamCardTitle}
|
||||
extra={
|
||||
<Button type="primary" onClick={() => form.submit()}>
|
||||
{t("general.actions.save")}
|
||||
@@ -210,86 +265,119 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.allocation_percentage")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "percentage"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.payout_method")}
|
||||
key={`${index}-payout-method`}
|
||||
name={[field.name, "payout_method"]}
|
||||
initialValue="hourly"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select options={payoutMethodOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item noStyle dependencies={[["employee_team_members", field.name, "payout_method"]]}>
|
||||
{() => {
|
||||
const payoutMethod =
|
||||
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly";
|
||||
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||
{fields.map((field, index) => {
|
||||
const teamMember = normalizeTeamMember(teamMembers[field.name]);
|
||||
|
||||
return LABOR_TYPES.map((laborType) => (
|
||||
<Form.Item
|
||||
label={payoutMethod === "commission" ? `${t(`joblines.fields.lbr_types.${laborType}`)} %` : t(`joblines.fields.lbr_types.${laborType}`)}
|
||||
key={`${index}-${fieldName}-${laborType}`}
|
||||
name={[field.name, fieldName, laborType]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
{payoutMethod === "commission" ? (
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
) : (
|
||||
<CurrencyInput />
|
||||
)}
|
||||
</Form.Item>
|
||||
));
|
||||
}}
|
||||
return (
|
||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
||||
<Form.Item label={t("employees.fields.id")} key={`${index}`} name={[field.name, "id"]} hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<LayoutFormRow
|
||||
grow
|
||||
title={getTeamMemberTitle(teamMember)}
|
||||
extra={
|
||||
<Space align="center" size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteFilled />}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.employee}>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.employeeid")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "employeeid"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.allocation}>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.allocation_percentage")}
|
||||
key={`${index}`}
|
||||
name={[field.name, "percentage"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col {...TEAM_MEMBER_PRIMARY_FIELD_COLS.payoutMethod}>
|
||||
<Form.Item
|
||||
label={t("employee_teams.fields.payout_method")}
|
||||
key={`${index}-payout-method`}
|
||||
name={[field.name, "payout_method"]}
|
||||
initialValue="hourly"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select options={payoutMethodOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item noStyle dependencies={[["employee_team_members", field.name, "payout_method"]]}>
|
||||
{() => {
|
||||
const payoutMethod =
|
||||
form.getFieldValue(["employee_team_members", field.name, "payout_method"]) || "hourly";
|
||||
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 0]}>
|
||||
{LABOR_TYPES.map((laborType) => (
|
||||
<Col {...TEAM_MEMBER_RATE_FIELD_COLS} key={`${index}-${fieldName}-${laborType}`}>
|
||||
<Form.Item
|
||||
label={
|
||||
t(`joblines.fields.lbr_types.${laborType}`)
|
||||
}
|
||||
name={[field.name, fieldName, laborType]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
{payoutMethod === "commission" ? (
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
) : (
|
||||
<CurrencyInput />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
|
||||
@@ -15,6 +15,18 @@ const mapDispatchToProps = () => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTicketTaskModalComponent);
|
||||
|
||||
const getPayoutMethodLabel = (payoutMethod, t) => {
|
||||
if (!payoutMethod) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (payoutMethod === "hourly" || payoutMethod === "commission") {
|
||||
return t(`timetickets.labels.payout_methods.${payoutMethod}`);
|
||||
}
|
||||
|
||||
return payoutMethod;
|
||||
};
|
||||
|
||||
export function TimeTicketTaskModalComponent({ bodyshop, form, loading, completedTasks, unassignedHours }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -101,45 +113,51 @@ export function TimeTicketTaskModalComponent({ bodyshop, form, loading, complete
|
||||
<th>{t("timetickets.fields.cost_center")}</th>
|
||||
<th>{t("timetickets.fields.ciecacode")}</th>
|
||||
<th>{t("timetickets.fields.productivehrs")}</th>
|
||||
<th>{t("timetickets.fields.payout_method")}</th>
|
||||
<th>{t("timetickets.fields.rate")}</th>
|
||||
<th>{t("timetickets.fields.amount")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
||||
<ReadOnlyFormItemComponent type="employee" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{fields.map((field, index) => {
|
||||
const payoutMethod = form.getFieldValue(["timetickets", field.name, "payout_context", "payout_method"]);
|
||||
|
||||
return (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item key={`${index}employeeid`} name={[field.name, "employeeid"]}>
|
||||
<ReadOnlyFormItemComponent type="employee" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}cost_center`} name={[field.name, "cost_center"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}ciecacode`} name={[field.name, "ciecacode"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}productivehrs`} name={[field.name, "productivehrs"]}>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>{getPayoutMethodLabel(payoutMethod, t)}</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}rate`} name={[field.name, "rate"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item key={`${index}payoutamount`} name={[field.name, "payoutamount"]}>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Alert type="success" title={t("timetickets.labels.payrollclaimedtasks")} />
|
||||
|
||||
@@ -25,6 +25,22 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TimeTickeTaskModalContainer);
|
||||
|
||||
const toFiniteNumber = (value) => {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const getPreviewPayoutAmount = (ticket) => {
|
||||
const productiveHours = toFiniteNumber(ticket?.productivehrs);
|
||||
const rate = toFiniteNumber(ticket?.rate);
|
||||
|
||||
if (productiveHours === null || rate === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return productiveHours * rate;
|
||||
};
|
||||
|
||||
export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicketTasksModal, toggleModalVisible }) {
|
||||
const [form] = Form.useForm();
|
||||
const { context, open, actions } = timeTicketTasksModal;
|
||||
@@ -93,7 +109,7 @@ export function TimeTickeTaskModalContainer({ currentUser, technician, timeTicke
|
||||
form.setFieldsValue({
|
||||
timetickets: (data.ticketsToInsert || []).map((ticket) => ({
|
||||
...ticket,
|
||||
payoutamount: Number(ticket.productivehrs || 0) * Number(ticket.rate || 0)
|
||||
payoutamount: getPreviewPayoutAmount(ticket)
|
||||
}))
|
||||
});
|
||||
setUnassignedHours(data.unassignedHours);
|
||||
|
||||
@@ -30,7 +30,6 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
|
||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||
import { lazyDev } from "../../utils/lazyWithPreload.jsx";
|
||||
import EsignatureModalContainer from "../../components/esignature-modal/esignature-modal.container.jsx";
|
||||
|
||||
const PrintCenterModalContainer = lazyDev(
|
||||
() => import("../../components/print-center-modal/print-center-modal.container")
|
||||
@@ -69,9 +68,7 @@ const FeatureRequestPage = lazyDev(() => import("../feature-request/feature-requ
|
||||
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 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(
|
||||
() => import("../../components/time-ticket-task-modal/time-ticket-task-modal.container")
|
||||
);
|
||||
@@ -113,9 +110,7 @@ const TtApprovals = lazyDev(() => import("../tt-approvals/tt-approvals.page.cont
|
||||
const MyTasksPage = lazyDev(() => import("../tasks/myTasksPageContainer.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 mapStateToProps = createStructuredSelector({
|
||||
@@ -183,7 +178,6 @@ export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode, cu
|
||||
<TaskUpsertModalContainer />
|
||||
<BreadCrumbs />
|
||||
<BillEnterModalContainer />
|
||||
<EsignatureModalContainer />
|
||||
<JobCostingModal />
|
||||
<ReportCenterModal />
|
||||
<EmailOverlayContainer />
|
||||
|
||||
@@ -27,8 +27,7 @@ const INITIAL_STATE = {
|
||||
contractFinder: { ...baseModal },
|
||||
inventoryUpsert: { ...baseModal },
|
||||
ca_bc_eftTableConvert: { ...baseModal },
|
||||
cardPayment: { ...baseModal },
|
||||
esignature: { ...baseModal }
|
||||
cardPayment: { ...baseModal }
|
||||
};
|
||||
|
||||
const modalsReducer = (state = INITIAL_STATE, action) => {
|
||||
|
||||
@@ -36,4 +36,3 @@ export const selectInventoryUpsert = createSelector([selectModals], (modals) =>
|
||||
export const selectCaBcEtfTableConvert = createSelector([selectModals], (modals) => modals.ca_bc_eftTableConvert);
|
||||
|
||||
export const selectCardPayment = createSelector([selectModals], (modals) => modals.cardPayment);
|
||||
export const selectEsignature = createSelector([selectModals], (modals) => modals.esignature);
|
||||
|
||||
@@ -1183,6 +1183,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"active": "Active",
|
||||
"allocation": "Allocation",
|
||||
"allocation_percentage": "Allocation %",
|
||||
"employeeid": "Employee",
|
||||
"max_load": "Max Load",
|
||||
@@ -1194,6 +1195,7 @@
|
||||
"allocation_total": "Allocation Total: {{total}}%"
|
||||
},
|
||||
"options": {
|
||||
"commission": "Commission",
|
||||
"commission_percentage": "Commission %",
|
||||
"hourly": "Hourly"
|
||||
}
|
||||
@@ -2203,7 +2205,6 @@
|
||||
"duplicateconfirm": "Are you sure you want to duplicate this Job? Some elements of this Job will not be duplicated.",
|
||||
"emailaudit": "Email Audit Trail",
|
||||
"employeeassignments": "Employee Assignments",
|
||||
"esignature": "E-Signature",
|
||||
"estimatelines": "Estimate Lines",
|
||||
"estimator": "Estimator",
|
||||
"existing_jobs": "Existing Jobs",
|
||||
@@ -3610,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "Actual Hours",
|
||||
"amount": "Amount",
|
||||
"ciecacode": "CIECA Code",
|
||||
"clockhours": "Clock Hours",
|
||||
"clockoff": "Clock Off",
|
||||
@@ -3625,7 +3627,9 @@
|
||||
"flat_rate": "Flat Rate?",
|
||||
"memo": "Memo",
|
||||
"pay": "Pay",
|
||||
"payout_method": "Payout Method",
|
||||
"productivehrs": "Productive Hours",
|
||||
"rate": "Rate",
|
||||
"ro_number": "Job to Post Against",
|
||||
"task_name": "Task"
|
||||
},
|
||||
@@ -3644,6 +3648,10 @@
|
||||
"lunch": "Lunch",
|
||||
"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": {
|
||||
"commission": "Commission",
|
||||
"hourly": "Hourly"
|
||||
},
|
||||
"pmbreak": "PM Break",
|
||||
"pmshift": "PM Shift",
|
||||
"shift": "Shift",
|
||||
|
||||
@@ -1183,6 +1183,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"active": "",
|
||||
"allocation": "",
|
||||
"allocation_percentage": "",
|
||||
"employeeid": "",
|
||||
"max_load": "",
|
||||
@@ -1194,6 +1195,7 @@
|
||||
"allocation_total": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
"commission_percentage": "",
|
||||
"hourly": ""
|
||||
}
|
||||
@@ -2203,7 +2205,6 @@
|
||||
"duplicateconfirm": "",
|
||||
"emailaudit": "",
|
||||
"employeeassignments": "",
|
||||
"esignature": "",
|
||||
"estimatelines": "",
|
||||
"estimator": "",
|
||||
"existing_jobs": "Empleos existentes",
|
||||
@@ -3610,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "",
|
||||
"amount": "",
|
||||
"ciecacode": "",
|
||||
"clockhours": "",
|
||||
"clockoff": "",
|
||||
@@ -3625,7 +3627,9 @@
|
||||
"flat_rate": "",
|
||||
"memo": "",
|
||||
"pay": "",
|
||||
"payout_method": "",
|
||||
"productivehrs": "",
|
||||
"rate": "",
|
||||
"ro_number": "",
|
||||
"task_name": ""
|
||||
},
|
||||
@@ -3644,6 +3648,10 @@
|
||||
"lunch": "",
|
||||
"new": "",
|
||||
"payrollclaimedtasks": "",
|
||||
"payout_methods": {
|
||||
"commission": "",
|
||||
"hourly": ""
|
||||
},
|
||||
"pmbreak": "",
|
||||
"pmshift": "",
|
||||
"shift": "",
|
||||
|
||||
@@ -1183,6 +1183,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"active": "",
|
||||
"allocation": "",
|
||||
"allocation_percentage": "",
|
||||
"employeeid": "",
|
||||
"max_load": "",
|
||||
@@ -1194,6 +1195,7 @@
|
||||
"allocation_total": ""
|
||||
},
|
||||
"options": {
|
||||
"commission": "",
|
||||
"commission_percentage": "",
|
||||
"hourly": ""
|
||||
}
|
||||
@@ -2203,7 +2205,6 @@
|
||||
"duplicateconfirm": "",
|
||||
"emailaudit": "",
|
||||
"employeeassignments": "",
|
||||
"esignature": "",
|
||||
"estimatelines": "",
|
||||
"estimator": "",
|
||||
"existing_jobs": "Emplois existants",
|
||||
@@ -3610,6 +3611,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"actualhrs": "",
|
||||
"amount": "",
|
||||
"ciecacode": "",
|
||||
"clockhours": "",
|
||||
"clockoff": "",
|
||||
@@ -3625,7 +3627,9 @@
|
||||
"flat_rate": "",
|
||||
"memo": "",
|
||||
"pay": "",
|
||||
"payout_method": "",
|
||||
"productivehrs": "",
|
||||
"rate": "",
|
||||
"ro_number": "",
|
||||
"task_name": ""
|
||||
},
|
||||
@@ -3644,6 +3648,10 @@
|
||||
"lunch": "",
|
||||
"new": "",
|
||||
"payrollclaimedtasks": "",
|
||||
"payout_methods": {
|
||||
"commission": "",
|
||||
"hourly": ""
|
||||
},
|
||||
"pmbreak": "",
|
||||
"pmshift": "",
|
||||
"shift": "",
|
||||
|
||||
@@ -47,33 +47,6 @@ const httpsCerts = {
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const pathSeparatorPattern = String.raw`[\\/]`;
|
||||
|
||||
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const packageChunkTest = (packageNames) => {
|
||||
const names = Array.isArray(packageNames) ? packageNames : [packageNames];
|
||||
|
||||
return new RegExp(
|
||||
`${pathSeparatorPattern}node_modules${pathSeparatorPattern}(?:${names
|
||||
.map((name) => name.split("/").map(escapeRegex).join(pathSeparatorPattern))
|
||||
.join("|")})(?:${pathSeparatorPattern}|$)`
|
||||
);
|
||||
};
|
||||
|
||||
const vendorCodeSplittingGroups = [
|
||||
{ name: "antd", test: packageChunkTest("antd"), priority: 100 },
|
||||
{ name: "react-redux", test: packageChunkTest("react-redux"), priority: 95 },
|
||||
{ name: "redux", test: packageChunkTest("redux"), priority: 90 },
|
||||
{ name: "lodash", test: packageChunkTest("lodash"), priority: 85 },
|
||||
{ name: "@sentry/react", test: packageChunkTest("@sentry/react"), priority: 80 },
|
||||
{ name: "@splitsoftware/splitio-react", test: packageChunkTest("@splitsoftware/splitio-react"), priority: 75 },
|
||||
{ name: "logrocket", test: packageChunkTest("logrocket"), priority: 70 },
|
||||
{ name: "firebase", test: packageChunkTest("@firebase"), priority: 65 },
|
||||
{ name: "markerjs2", test: packageChunkTest("markerjs2"), priority: 60 },
|
||||
{ name: "@apollo/client", test: packageChunkTest("@apollo/client"), priority: 55 },
|
||||
{ name: "libphonenumber-js", test: packageChunkTest("libphonenumber-js"), priority: 50 },
|
||||
{ name: "recharts", test: packageChunkTest("recharts"), priority: 45 }
|
||||
];
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
// React Compiler is always enabled for production/test builds
|
||||
@@ -255,13 +228,27 @@ export default defineConfig(({ command, mode }) => {
|
||||
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rolldownOptions: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
groups: vendorCodeSplittingGroups
|
||||
},
|
||||
comments: {
|
||||
legal: false
|
||||
manualChunks: {
|
||||
antd: ["antd"],
|
||||
"react-redux": ["react-redux"],
|
||||
redux: ["redux"],
|
||||
lodash: ["lodash"],
|
||||
"@sentry/react": ["@sentry/react"],
|
||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||
logrocket: ["logrocket"],
|
||||
firebase: [
|
||||
"@firebase/analytics",
|
||||
"@firebase/app",
|
||||
"@firebase/firestore",
|
||||
"@firebase/auth",
|
||||
"@firebase/messaging"
|
||||
],
|
||||
markerjs2: ["markerjs2"],
|
||||
"@apollo/client": ["@apollo/client"],
|
||||
"libphonenumber-js": ["libphonenumber-js"],
|
||||
recharts: ["recharts"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -269,6 +256,12 @@ export default defineConfig(({ command, mode }) => {
|
||||
cssMinify: "lightningcss"
|
||||
},
|
||||
|
||||
// Strip console/debugger in prod to shrink bundles
|
||||
esbuild: {
|
||||
// drop: mode === "production" ? ["console", "debugger"] : [],
|
||||
legalComments: "none" // Remove license comments in production
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
"react",
|
||||
@@ -291,8 +284,8 @@ export default defineConfig(({ command, mode }) => {
|
||||
"@firebase/util",
|
||||
"styled-components"
|
||||
],
|
||||
rolldownOptions: {
|
||||
moduleTypes: { ".jsx": "jsx", ".tsx": "tsx" }
|
||||
esbuildOptions: {
|
||||
loader: { ".jsx": "jsx", ".tsx": "tsx" }
|
||||
},
|
||||
// Force styled-components to be pre-bundled and deduplicated
|
||||
force: mode === "development"
|
||||
|
||||
837
package-lock.json
generated
837
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,6 @@
|
||||
"@aws-sdk/credential-provider-node": "^3.972.21",
|
||||
"@aws-sdk/lib-storage": "^3.1009.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1009.0",
|
||||
"@documenso/sdk-typescript": "^0.8.0",
|
||||
"@jsreport/nodejs-client": "^4.1.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
|
||||
@@ -130,7 +130,6 @@ const applyRoutes = ({ app }) => {
|
||||
app.use("/ai", require("./server/routes/aiRoutes"));
|
||||
|
||||
app.use("/chatter", require("./server/routes/chatterRoutes"));
|
||||
app.use("/esign", require("./server/routes/esignRoutes"));
|
||||
|
||||
// Default route for forbidden access
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
|
||||
const { Documenso } = require("@documenso/sdk-typescript");
|
||||
const axios = require("axios");
|
||||
const { jsrAuthString } = require("../utils/utils");
|
||||
const logger = require("../utils/logger");
|
||||
const DOCUMENSO_API_KEY = "api_asojim0czruv13ud";//Done on a by team basis,
|
||||
const documenso = new Documenso({
|
||||
apiKey: DOCUMENSO_API_KEY,//Done on a by team basis,
|
||||
serverURL: "https://stg-app.documenso.com/api/v2",
|
||||
});
|
||||
const JSR_SERVER = "https://reports.test.imex.online";
|
||||
const jsreport = require("@jsreport/nodejs-client");
|
||||
const { QUERY_JOB_FOR_SIGNATURE, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
||||
|
||||
|
||||
async function distributeDocument(req, res) {
|
||||
try {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
const { documentId } = req.body;
|
||||
const distributeResult = await documenso.documents.distribute({
|
||||
documentId,
|
||||
});
|
||||
|
||||
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||
obj: {
|
||||
jobid: req.body.jobid,
|
||||
bodyshopid: req.body.bodyshopid,
|
||||
operation: `Esignature document with title ${distributeResult.title} (ID: ${documentId}) distributed to recipients.`,
|
||||
useremail: req.user?.email,
|
||||
type: 'esig-distribute'
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ success: true, distributeResult });
|
||||
} catch (error) {
|
||||
console.error("Error distributing document:", error?.data);
|
||||
logger.log(`esig-distribute-error`, "ERROR", "esig", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
body: req.body
|
||||
});
|
||||
res.status(500).json({ error: "An error occurred while distributing the document." });
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDocument(req, res) {
|
||||
try {
|
||||
const { documentId } = req.body;
|
||||
//TODO: This needs to be hardened to prevent deleting other people's documents, completed ones, etc.
|
||||
const deleteResult = await documenso.documents.delete({
|
||||
documentId
|
||||
});
|
||||
res.json({ success: true, deleteResult });
|
||||
} catch (error) {
|
||||
console.error("Error deleting document:", error?.data);
|
||||
logger.log(`esig-delete-error`, "ERROR", "esig", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
body: req.body
|
||||
});
|
||||
res.status(500).json({ error: "An error occurred while deleting the document." });
|
||||
}
|
||||
}
|
||||
|
||||
async function newEsignDocument(req, res) {
|
||||
try {
|
||||
const client = req.userGraphQLClient;
|
||||
const { bodyshop } = req.body
|
||||
const { pdf: fileBuffer, esigData } = await RenderTemplate({ client, req })
|
||||
const fileBlob = new Blob([fileBuffer], { type: "application/pdf" });
|
||||
|
||||
|
||||
//Get the Job data.
|
||||
const { jobs_by_pk: jobData } = await client.request(QUERY_JOB_FOR_SIGNATURE, { jobid: req.body.jobid });
|
||||
|
||||
const createDocumentResponse = await documenso.documents.create({
|
||||
payload: {
|
||||
title: esigData?.title,
|
||||
externalId: `${req.body.jobid}|${req.user?.email}`, //Have to pass the uploaded by later on. Limited to 255 chars.
|
||||
recipients: [
|
||||
{
|
||||
email: "patrick@imexsystems.ca",//jobData.ownr_ea,
|
||||
name: `${jobData.ownr_fn} ${jobData.ownr_ln}`,
|
||||
role: "SIGNER",
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
timezone: bodyshop.timezone,
|
||||
dateFormat: "MM/dd/yyyy hh:mm a",
|
||||
language: "en",
|
||||
subject: esigData?.subject,
|
||||
message: esigData?.message,
|
||||
|
||||
}
|
||||
},
|
||||
file: fileBlob
|
||||
});
|
||||
|
||||
const documentResult = await documenso.documents.get({
|
||||
documentId: createDocumentResponse.id,
|
||||
});
|
||||
|
||||
|
||||
if (esigData?.fields && esigData.fields.length > 0) {
|
||||
try {
|
||||
await documenso.envelopes.fields.createMany({
|
||||
envelopeId: createDocumentResponse.envelopeId,
|
||||
data: esigData.fields.map(sigField => ({ ...sigField, recipientId: documentResult.recipients[0].id, }))
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log(`esig-new-fields-error`, "ERROR", "esig", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
body: req.body
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const presignToken = await documenso.embedding.embeddingPresignCreateEmbeddingPresignToken({})
|
||||
|
||||
//add to job audit trail.
|
||||
|
||||
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||
obj: {
|
||||
jobid: req.body.jobid,
|
||||
bodyshopid: bodyshop.id,
|
||||
operation: `Esignature document created. Subject: ${esigData?.subject || "No subject"}, Message: ${esigData?.message || "No message"}. Document ID: ${createDocumentResponse.id} Envlope ID: ${createDocumentResponse.envelopeId}`,
|
||||
useremail: req.user?.email,
|
||||
type: 'esig-create'
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ token: presignToken.token, documentId: createDocumentResponse.id, envelopeId: createDocumentResponse.envelopeId });
|
||||
}
|
||||
catch (error) {
|
||||
logger.log(`esig-new-error`, "ERROR", "esig", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
body: req.body
|
||||
});
|
||||
res.status(500).json({ error: "An error occurred while creating the e-sign document." });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function RenderTemplate({ req }) {
|
||||
//TODO Refactor to pull
|
||||
const jsrAuth = jsrAuthString()
|
||||
|
||||
const jsreportClient = new jsreport("https://reports.test.imex.online", process.env.JSR_USER, process.env.JSR_PASSWORD);
|
||||
const { templateObject, bodyshop } = req.body;
|
||||
let { contextData, useShopSpecificTemplate, shopSpecificFolder, esigData } = await fetchContextData({ templateObject, jsrAuth, req });
|
||||
|
||||
const { ignoreCustomMargins } = { ignoreCustomMargins: false }// Templates[templateObject.name];
|
||||
let reportRequest = {
|
||||
template: {
|
||||
name: useShopSpecificTemplate ? `/${bodyshop.imexshopid}/${templateObject.name}` : `/${templateObject.name}`,
|
||||
|
||||
recipe: "chrome-pdf",
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...contextData,
|
||||
...templateObject.variables,
|
||||
...templateObject.context,
|
||||
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
|
||||
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
|
||||
bodyshop: bodyshop,
|
||||
filters: templateObject?.filters,
|
||||
sorters: templateObject?.sorters,
|
||||
offset: bodyshop.timezone, //dayjs().utcOffset(),
|
||||
defaultSorters: templateObject?.defaultSorters
|
||||
}
|
||||
};
|
||||
const render = await jsreportClient.render(reportRequest);
|
||||
|
||||
//Check render object and download. It should be the PDF?
|
||||
const pdfBuffer = await render.body()
|
||||
return { pdf: pdfBuffer, esigData }
|
||||
}
|
||||
|
||||
const fetchContextData = async ({ templateObject, jsrAuth, req, }) => {
|
||||
const { bodyshop } = req.body
|
||||
|
||||
|
||||
const folders = await axios.get(`${JSR_SERVER}/odata/folders`, {
|
||||
headers: { Authorization: jsrAuth }
|
||||
});
|
||||
const shopSpecificFolder = folders.data.value.find((f) => f.name === bodyshop.imexshopid);
|
||||
|
||||
const jsReportQueries = await axios.get(
|
||||
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||
{ headers: { Authorization: jsrAuth } }
|
||||
);
|
||||
const jsReportEsig = await axios.get(
|
||||
`${JSR_SERVER}/odata/assets?$filter=name eq '${templateObject.name}.esig'`,
|
||||
{ headers: { Authorization: jsrAuth } }
|
||||
);
|
||||
|
||||
let templateQueryToExecute;
|
||||
let esigData;
|
||||
let useShopSpecificTemplate = false;
|
||||
// let shopSpecificTemplate;
|
||||
|
||||
if (shopSpecificFolder) {
|
||||
let shopSpecificTemplate = jsReportQueries.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificTemplate) {
|
||||
useShopSpecificTemplate = true;
|
||||
templateQueryToExecute = atob(shopSpecificTemplate.content);
|
||||
}
|
||||
let shopSpecificEsig = jsReportEsig.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificEsig) {
|
||||
esigData = (atob(shopSpecificEsig.content));
|
||||
}
|
||||
}
|
||||
|
||||
if (!templateQueryToExecute) {
|
||||
const generalTemplate = jsReportQueries.data.value.find((f) => !f.folder);
|
||||
useShopSpecificTemplate = false;
|
||||
templateQueryToExecute = atob(generalTemplate.content);
|
||||
}
|
||||
if (!esigData) {
|
||||
const generalTemplate = jsReportEsig.data.value.find((f) => !f.folder);
|
||||
useShopSpecificTemplate = false;
|
||||
if (generalTemplate && generalTemplate.content) {
|
||||
esigData = atob(generalTemplate?.content);
|
||||
}
|
||||
}
|
||||
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
|
||||
// In the print center, we will never have sorters or filters.
|
||||
// We have no template filters or sorters, so we can just execute the query and return the data
|
||||
// if (!hasFilters && !hasSorters && !hasDefaultSorters) {
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const data = await client.request(
|
||||
templateQueryToExecute,
|
||||
templateObject.variables,
|
||||
);
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
let parsedEsigData
|
||||
try {
|
||||
parsedEsigData = esigData ? JSON.parse(esigData) : null;
|
||||
} catch (error) {
|
||||
console.log("Error parsing esig data", error);
|
||||
parsedEsigData = {}
|
||||
}
|
||||
|
||||
return {
|
||||
contextData,
|
||||
useShopSpecificTemplate,
|
||||
shopSpecificFolder,
|
||||
esigData: parsedEsigData
|
||||
};
|
||||
// }
|
||||
|
||||
// return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
newEsignDocument,
|
||||
distributeDocument,
|
||||
deleteDocument
|
||||
}
|
||||
|
||||
|
||||
|
||||
// const sample_esig_for_jsr = {
|
||||
// "fields": [
|
||||
// {
|
||||
// "placeholder": "[[signature]]",
|
||||
// "type": "SIGNATURE"
|
||||
// },
|
||||
// {
|
||||
// "placeholder": "[[date]]",
|
||||
// "type": "DATE"
|
||||
// }
|
||||
// ],
|
||||
// "subject": "CASL Auth Set in JSR",
|
||||
// "message": "CASL Message set in JSR",
|
||||
// "title": "CASL Title set in JSR"
|
||||
// }
|
||||
@@ -1,393 +0,0 @@
|
||||
|
||||
const { Documenso } = require("@documenso/sdk-typescript");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const logger = require("../utils/logger");
|
||||
const { QUERY_META_FOR_ESIG_COMPLETION, INSERT_ESIGNATURE_DOCUMENT, INSERT_ESIG_AUDIT_TRAIL } = require("../graphql-client/queries");
|
||||
const { uploadFileBuffer } = require("../media/imgproxy-media");
|
||||
const client = require('../graphql-client/graphql-client').client;
|
||||
const documenso = new Documenso({
|
||||
apiKey: "api_asojim0czruv13ud",//Done on a by team basis,
|
||||
serverURL: "https://stg-app.documenso.com/api/v2",
|
||||
});
|
||||
|
||||
const webhookTypeEnums = {
|
||||
DOCUMENT_CREATED: "DOCUMENT_CREATED",
|
||||
DOCUMENT_SENT: "DOCUMENT_SENT",
|
||||
DOCUMENT_COMPLETED: "DOCUMENT_COMPLETED",
|
||||
DOCUMENT_REJECTED: "DOCUMENT_REJECTED",
|
||||
DOCUMENT_CANCELLED: "DOCUMENT_CANCELLED",
|
||||
DOCUMENT_OPENED: "DOCUMENT_OPENED",
|
||||
DOCUMENT_SIGNED: "DOCUMENT_SIGNED",
|
||||
}
|
||||
|
||||
async function esignWebhook(req, res) {
|
||||
console.log("Esign Webhook Received:", req.body);
|
||||
try {
|
||||
const message = req.body
|
||||
logger.log(`esig-webhook-received`, "DEBUG", "redis", "api", {
|
||||
event: message.event,
|
||||
body: message
|
||||
});
|
||||
|
||||
switch (message.event) {
|
||||
case webhookTypeEnums.DOCUMENT_CREATED:
|
||||
//This is largely a throwaway event we know it was created.
|
||||
console.log("Document created event received. Document ID:", message.payload.documentId);
|
||||
// Here you can add any additional processing you want to do when a document is created
|
||||
break;
|
||||
case webhookTypeEnums.DOCUMENT_COMPLETED:
|
||||
console.log("Document completed event received. Document ID:", message.payload.documentId);
|
||||
await handleDocumentCompleted(message.payload);
|
||||
// Here you can add any additional processing you want to do when a document is completed
|
||||
break;
|
||||
case webhookTypeEnums.DOCUMENT_SIGNED:
|
||||
console.log("Document signed event received. Document ID:", message.payload.documentId);
|
||||
// Here you can add any additional processing you want to do when a document is signed
|
||||
break;
|
||||
default:
|
||||
console.log(`Unhandled event type: ${message.event}`);
|
||||
}
|
||||
|
||||
// const result = await documenso.documents.download({
|
||||
// documentId: req.body.payload.id,
|
||||
// });
|
||||
// result.resultingBuffer = Buffer.from(result.resultingArrayBuffer);
|
||||
// // Save the document to a file for testing purposes
|
||||
// const downloadsDir = path.join(__dirname, '../downloads');
|
||||
// if (!fs.existsSync(downloadsDir)) {
|
||||
// fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
// }
|
||||
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
|
||||
// fs.writeFileSync(filePath, result.resultingBuffer);
|
||||
|
||||
// console.log(result)
|
||||
|
||||
res.sendStatus(200)
|
||||
} catch (error) {
|
||||
logger.log(`esig-webhook-error`, "ERROR", "redis", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
body: req.body
|
||||
});
|
||||
// const downloadsDir = path.join(__dirname, '../downloads');
|
||||
// if (!fs.existsSync(downloadsDir)) {
|
||||
// fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
// }
|
||||
// const filePath = path.join(downloadsDir, `document_${req.body.payload.id}.pdf`);
|
||||
// fs.writeFileSync(filePath, Buffer.from(err.body));
|
||||
// console.error("Error handling esign webhook:", err);
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDocumentCompleted(payload = sampleComplete) {
|
||||
|
||||
|
||||
//Check if the bodyshop is on image proxy or not
|
||||
try {
|
||||
//Split the external id to get the uploaded user.
|
||||
const [jobid, uploaded_by] = payload.externalId.split("|");
|
||||
|
||||
if (!jobid || !uploaded_by) {
|
||||
throw new Error(`Invalid externalId format. Expected "jobid|uploaded_by", got "${payload.externalId}"`);
|
||||
}
|
||||
const { jobs_by_pk } = await client.request(QUERY_META_FOR_ESIG_COMPLETION, {
|
||||
jobid
|
||||
});
|
||||
const document = await documenso.document.documentDownload({
|
||||
documentId: payload.id,
|
||||
});
|
||||
|
||||
const response = await fetch(document.downloadUrl);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
|
||||
let key = `${jobs_by_pk.bodyshop.id}/${jobs_by_pk.id}/${replaceAccents(document.filename).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.pdf`;
|
||||
|
||||
if (jobs_by_pk?.bodyshop?.uselocalmediaserver) {
|
||||
//LMS not yet implemented.
|
||||
|
||||
} else {
|
||||
//S3 Upload
|
||||
const uploadResult = await uploadFileBuffer({ key, buffer, contentType: "application/pdf" });
|
||||
if (!uploadResult.success) {
|
||||
logger.log(`esig-webhook-s3-upload-error`, "ERROR", "redis", "api", {
|
||||
message: uploadResult.message,
|
||||
stack: uploadResult.stack,
|
||||
jobid: jobid,
|
||||
documentId: payload.id
|
||||
});
|
||||
} else {
|
||||
logger.log(`esig-webhook-s3-upload-success`, "INFO", "redis", "api", {
|
||||
jobid: jobid,
|
||||
documentId: payload.id,
|
||||
s3Key: key,
|
||||
bucket: uploadResult.bucket
|
||||
});
|
||||
const auditEntry = await client.request(INSERT_ESIG_AUDIT_TRAIL, {
|
||||
obj: {
|
||||
jobid: jobs_by_pk.id,
|
||||
bodyshopid: jobs_by_pk.bodyshop.id,
|
||||
operation: `Esignature document with title ${payload.title} (ID: ${payload.documentMeta.id}) has been completed.`,
|
||||
useremail: uploaded_by,
|
||||
type: 'esig-complete'
|
||||
}
|
||||
})
|
||||
//insert the document record with the s3 key and bucket info.
|
||||
await client.request(INSERT_ESIGNATURE_DOCUMENT, {
|
||||
docInput: {
|
||||
jobid: jobs_by_pk.id,
|
||||
uploaded_by: uploaded_by,
|
||||
key,
|
||||
type: "application/pdf",
|
||||
extension: "pdf",
|
||||
bodyshopid: jobs_by_pk.bodyshop.id,
|
||||
size: buffer.length,
|
||||
takenat: new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.log(`esig-webhook-event-completed-error`, "ERROR", "redis", "api", {
|
||||
message: error.message, stack: error.stack,
|
||||
payload
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
esignWebhook
|
||||
}
|
||||
|
||||
|
||||
const sampleComplete = {
|
||||
"id": 10929,
|
||||
"title": "CASL Title set in JSR",
|
||||
"source": "DOCUMENT",
|
||||
"status": "COMPLETED",
|
||||
"teamId": 742,
|
||||
"userId": 654,
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 24997,
|
||||
"name": "James Tschetter",
|
||||
"role": "SIGNER",
|
||||
"email": "patrick@imexsystems.ca",
|
||||
"token": "uMom0GwL29NBqMfohGpUE",
|
||||
"signedAt": "2026-02-27T22:11:52.835Z",
|
||||
"expiresAt": "2026-05-28T22:10:48.991Z",
|
||||
"documentId": 10929,
|
||||
"readStatus": "OPENED",
|
||||
"sendStatus": "SENT",
|
||||
"templateId": null,
|
||||
"authOptions": {
|
||||
"accessAuth": [],
|
||||
"actionAuth": []
|
||||
},
|
||||
"signingOrder": null,
|
||||
"signingStatus": "SIGNED",
|
||||
"rejectionReason": null,
|
||||
"documentDeletedAt": null,
|
||||
"expirationNotifiedAt": null
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-02-27T22:10:10.580Z",
|
||||
"deletedAt": null,
|
||||
"updatedAt": "2026-02-27T22:11:57.753Z",
|
||||
"externalId": null,
|
||||
"formValues": null,
|
||||
"recipients": [
|
||||
{
|
||||
"id": 24997,
|
||||
"name": "James Tschetter",
|
||||
"role": "SIGNER",
|
||||
"email": "patrick@imexsystems.ca",
|
||||
"token": "uMom0GwL29NBqMfohGpUE",
|
||||
"signedAt": "2026-02-27T22:11:52.835Z",
|
||||
"expiresAt": "2026-05-28T22:10:48.991Z",
|
||||
"documentId": 10929,
|
||||
"readStatus": "OPENED",
|
||||
"sendStatus": "SENT",
|
||||
"templateId": null,
|
||||
"authOptions": {
|
||||
"accessAuth": [],
|
||||
"actionAuth": []
|
||||
},
|
||||
"signingOrder": null,
|
||||
"signingStatus": "SIGNED",
|
||||
"rejectionReason": null,
|
||||
"documentDeletedAt": null,
|
||||
"expirationNotifiedAt": null
|
||||
}
|
||||
],
|
||||
"templateId": null,
|
||||
"visibility": "EVERYONE",
|
||||
"authOptions": {
|
||||
"globalAccessAuth": [],
|
||||
"globalActionAuth": []
|
||||
},
|
||||
"completedAt": "2026-02-27T22:11:57.752Z",
|
||||
"documentMeta": {
|
||||
"id": "cmm5g3y7u00ecad1sv3ague1w",
|
||||
"message": "CASL Message set in JSR",
|
||||
"subject": "CASL Auth Set in JSR",
|
||||
"language": "en",
|
||||
"timezone": "Etc/UTC",
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"emailSettings": {
|
||||
"documentDeleted": true,
|
||||
"documentPending": true,
|
||||
"recipientSigned": true,
|
||||
"recipientRemoved": true,
|
||||
"documentCompleted": true,
|
||||
"ownerDocumentCompleted": true,
|
||||
"recipientSigningRequest": true
|
||||
},
|
||||
"distributionMethod": "EMAIL",
|
||||
"drawSignatureEnabled": true,
|
||||
"typedSignatureEnabled": true,
|
||||
"allowDictateNextSigner": false,
|
||||
"uploadSignatureEnabled": true
|
||||
}
|
||||
}
|
||||
// const sampleBody = {
|
||||
// event: "DOCUMENT_COMPLETED",
|
||||
// payload: {
|
||||
// Recipient: [
|
||||
// {
|
||||
// authOptions: {
|
||||
// accessAuth: [
|
||||
// ],
|
||||
// actionAuth: [
|
||||
// ],
|
||||
// },
|
||||
// documentDeletedAt: null,
|
||||
// documentId: 9827,
|
||||
// email: "patrick@imexsystems.ca",
|
||||
// expired: null,
|
||||
// id: 13311,
|
||||
// name: "Customer Fullname",
|
||||
// readStatus: "OPENED",
|
||||
// rejectionReason: null,
|
||||
// role: "SIGNER",
|
||||
// sendStatus: "SENT",
|
||||
// signedAt: "2026-01-30T18:29:12.648Z",
|
||||
// signingOrder: null,
|
||||
// signingStatus: "SIGNED",
|
||||
// templateId: null,
|
||||
// token: "uiEWIsXUPTbWHd7QedVgt",
|
||||
// },
|
||||
// ],
|
||||
// authOptions: {
|
||||
// globalAccessAuth: [
|
||||
// ],
|
||||
// globalActionAuth: [
|
||||
// ],
|
||||
// },
|
||||
// completedAt: "2026-01-30T18:29:16.279Z",
|
||||
// createdAt: "2026-01-30T18:28:48.861Z",
|
||||
// deletedAt: null,
|
||||
// documentMeta: {
|
||||
// allowDictateNextSigner: false,
|
||||
// dateFormat: "yyyy-MM-dd hh:mm a",
|
||||
// distributionMethod: "EMAIL",
|
||||
// drawSignatureEnabled: true,
|
||||
// emailSettings: {
|
||||
// documentCompleted: true,
|
||||
// documentDeleted: true,
|
||||
// documentPending: true,
|
||||
// ownerDocumentCompleted: true,
|
||||
// recipientRemoved: false,
|
||||
// recipientSigned: true,
|
||||
// recipientSigningRequest: true,
|
||||
// },
|
||||
// id: "cml17vfb200qjad1t2spxnc1n",
|
||||
// language: "en",
|
||||
// message: "To perform repairs on your vehicle, we must receive digital authorization. Please review and sign the document to proceed with repairs. ",
|
||||
// redirectUrl: null,
|
||||
// signingOrder: "PARALLEL",
|
||||
// subject: "Repair Authorization for ABC Collision",
|
||||
// timezone: "Etc/UTC",
|
||||
// typedSignatureEnabled: true,
|
||||
// uploadSignatureEnabled: true,
|
||||
// },
|
||||
// externalId: null,
|
||||
// formValues: null,
|
||||
// id: 9827,
|
||||
// recipients: [
|
||||
// {
|
||||
// authOptions: {
|
||||
// accessAuth: [
|
||||
// ],
|
||||
// actionAuth: [
|
||||
// ],
|
||||
// },
|
||||
// documentDeletedAt: null,
|
||||
// documentId: 9827,
|
||||
// email: "patrick@imexsystems.ca",
|
||||
// expired: null,
|
||||
// id: 13311,
|
||||
// name: "Customer Fullname",
|
||||
// readStatus: "OPENED",
|
||||
// rejectionReason: null,
|
||||
// role: "SIGNER",
|
||||
// sendStatus: "SENT",
|
||||
// signedAt: "2026-01-30T18:29:12.648Z",
|
||||
// signingOrder: null,
|
||||
// signingStatus: "SIGNED",
|
||||
// templateId: null,
|
||||
// token: "uiEWIsXUPTbWHd7QedVgt",
|
||||
// },
|
||||
// ],
|
||||
// source: "DOCUMENT",
|
||||
// status: "COMPLETED",
|
||||
// teamId: 742,
|
||||
// templateId: null,
|
||||
// title: "Repair Authorization - 1/30/2026, 6:28:48 PM",
|
||||
// updatedAt: "2026-01-30T18:29:16.280Z",
|
||||
// userId: 654,
|
||||
// visibility: "EVERYONE",
|
||||
// },
|
||||
// createdAt: "2026-01-30T18:29:18.504Z",
|
||||
// webhookEndpoint: "https://dev.patrickfic.com/esign/webhook",
|
||||
// }
|
||||
|
||||
function replaceAccents(str) {
|
||||
// Verifies if the String has accents and replace them
|
||||
if (str.search(/[\xC0-\xFF]/g) > -1) {
|
||||
str = str
|
||||
.replace(/[\xC0-\xC5]/g, "A")
|
||||
.replace(/[\xC6]/g, "AE")
|
||||
.replace(/[\xC7]/g, "C")
|
||||
.replace(/[\xC8-\xCB]/g, "E")
|
||||
.replace(/[\xCC-\xCF]/g, "I")
|
||||
.replace(/[\xD0]/g, "D")
|
||||
.replace(/[\xD1]/g, "N")
|
||||
.replace(/[\xD2-\xD6\xD8]/g, "O")
|
||||
.replace(/[\xD9-\xDC]/g, "U")
|
||||
.replace(/[\xDD]/g, "Y")
|
||||
.replace(/[\xDE]/g, "P")
|
||||
.replace(/[\xE0-\xE5]/g, "a")
|
||||
.replace(/[\xE6]/g, "ae")
|
||||
.replace(/[\xE7]/g, "c")
|
||||
.replace(/[\xE8-\xEB]/g, "e")
|
||||
.replace(/[\xEC-\xEF]/g, "i")
|
||||
.replace(/[\xF1]/g, "n")
|
||||
.replace(/[\xF2-\xF6\xF8]/g, "o")
|
||||
.replace(/[\xF9-\xFC]/g, "u")
|
||||
.replace(/[\xFE]/g, "p")
|
||||
.replace(/[\xFD\xFF]/g, "y");
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
`Unexpected Status or Content-Type: Status 200 Content-Type application/pdf\nBody: %PDF-1.7\n%<25><><EFBFBD><EFBFBD>\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Names 74 0 R\n/Dests 75 0 R\n/Info 77 0 R\n/Lang (en-US)\n/Version /1.7\n>>\nendobj\n77 0 obj\n<<\n/Type /Info\n/CreationDate (D:20260227230617Z00'00')\n/Producer <FEFF007000640066002D006C006900620020002800680074007400700073003A002F002F006700690074006800750062002E0063006F006D002F0048006F007000640069006E0067002F007000640066002D006C006900620029>\n/ModDate (D:20260227231057Z)…<>5[<5B>><3E>Wu7<><37>V<EFBFBD><56><EFBFBD><EFBFBD><EFBFBD>Pw<50>WX<57>ܮJ'6NWg<57>vYϳ<><CFB3><EFBFBD><EFBFBD><EFBFBD>Щr<D0A9>\n\t+<2B>1<EFBFBD><10>m{휑<0C>hwb<><62><EFBFBD>8<EFBFBD><38>qy<>1e<31>)۱<>5m<35><6D><08><>MVM!<21>m<EFBFBD>[A<><41><10>{l<><6C>\t<EFBFBD>hia4<61><34>Tm<54><6D>8<><38>a<>e<EFBFBD>}<7D>߫<><DFAB><15>]MVpяG<D18F><47>֏<EFBFBD>jJ<"<22>A<EFBFBD>mO*<2A>P<EFBFBD><0B><><><7F><EFBFBD><EFBFBD>ѧЛ\nendstream\nendobj\n26 0 obj\n<<\n/Length 478/Filter /FlateDecode\n>>\nstream\nx<EFBFBD>MSK<EFBFBD>9<08><>)<29><>*<04>O<EFBFBD>i<EFBFBD><69>,<2C><>o <20><>kS%<25>$<EFBFBD><EFBFBD>hR\rS'<27>I<EFBFBD><49>~<7E><03><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T[/<2F>{<05>k<EFBFBD>FC#<23><>֛<><D69B><EFBFBD>;Ӏ<>[<5B>⫀m<E2AB80>|Q<1F><>\x1b<EFBFBD><16>><3E>R<><52><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD>E#<23>pI<70><49>._H<5F>ᆫt<E186AB>k<EFBFBD>D3p<33>I<EFBFBD><49><EFBFBD><EFBFBD><01>W2<57><32><EFBFBD>oJ0<4A>j<EFBFBD><6A><EFBFBD>j#<23><>!<21>$<EFBFBD><EFBFBD>-<2D><08><><EFBFBD><EFBFBD><EFBFBD>.Ϋ<><CEAB><EFBFBD>TI|8D<38>H<1C><>Y<EFBFBD><59>x<EFBFBD><78><EFBFBD><EFBFBD>1<EFBFBD>73%<25>u<EFBFBD>T<EFBFBD><54>Ӑ.rcb<63>x<EFBFBD><78>Dd6=<3D><>Oڏ1^<5E>-<2D>...and 252354 more chars`
|
||||
@@ -1,95 +0,0 @@
|
||||
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;
|
||||
@@ -2253,16 +2253,18 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
|
||||
|
||||
exports.INSERT_NEW_TRANSITION = (
|
||||
includeOldTransition
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
|
||||
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
}) {
|
||||
insert_transitions_one(object: $newTransition) {
|
||||
id
|
||||
}
|
||||
${includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
${
|
||||
includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
affected_rows
|
||||
}`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -3254,46 +3256,3 @@ exports.SET_JOB_DMS_ID = `mutation SetJobDmsId($id: uuid!, $dms_id: String!, $dm
|
||||
kmin
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
exports.QUERY_JOB_FOR_SIGNATURE = `query QUERY_JOB_FOR_SIGNATURE($jobid: uuid!) {
|
||||
jobs_by_pk(id: $jobid) {
|
||||
id
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_ph1
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
exports.INSERT_ESIG_AUDIT_TRAIL = `mutation INSERT_ESIG_AUDIT_TRAIL($obj: audit_trail_insert_input!) {
|
||||
insert_audit_trail_one(object: $obj) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
exports.QUERY_META_FOR_ESIG_COMPLETION = `query QUERY_META_FOR_ESIG_COMPLETION($jobid: uuid!) {
|
||||
jobs_by_pk(id: $jobid) {
|
||||
id
|
||||
ro_number
|
||||
bodyshop {
|
||||
id
|
||||
uselocalmediaserver
|
||||
localmediatoken
|
||||
localmediaserverhttp
|
||||
localmediaservernetwork
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
exports.INSERT_ESIGNATURE_DOCUMENT = `mutation INSERT_ESIGNATURE_DOCUMENT($docInput: documents_insert_input!) {
|
||||
insert_documents_one(object: $docInput) {
|
||||
id
|
||||
name
|
||||
key
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -94,47 +94,6 @@ const generateSignedUploadUrls = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file buffer directly to S3.
|
||||
* Accepts either `req.file.buffer` (e.g. from multer) or `req.body.buffer` (base64 string).
|
||||
*/
|
||||
const uploadFileBuffer = async ({ key, contentType, buffer }) => {
|
||||
try {
|
||||
|
||||
|
||||
if (!key) {
|
||||
throw new Error("key is required");
|
||||
}
|
||||
if (!buffer) {
|
||||
throw new Error("buffer is required");
|
||||
}
|
||||
|
||||
const isPdf = key.toLowerCase().endsWith(".pdf");
|
||||
const client = new S3Client({ region: InstanceRegion() });
|
||||
|
||||
const putParams = {
|
||||
Bucket: imgproxyDestinationBucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
StorageClass: "INTELLIGENT_TIERING"
|
||||
};
|
||||
|
||||
if (contentType) {
|
||||
putParams.ContentType = contentType;
|
||||
} else if (isPdf) {
|
||||
putParams.ContentType = "application/pdf";
|
||||
}
|
||||
|
||||
await client.send(new PutObjectCommand(putParams));
|
||||
|
||||
|
||||
return ({ success: true, key, bucket: imgproxyDestinationBucket });
|
||||
} catch (error) {
|
||||
|
||||
return { success: false, message: error.message, stack: error.stack };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Thumbnail URLS
|
||||
* @param req
|
||||
@@ -541,7 +500,6 @@ const keyStandardize = (doc) => {
|
||||
|
||||
module.exports = {
|
||||
generateSignedUploadUrls,
|
||||
uploadFileBuffer,
|
||||
getThumbnailUrls,
|
||||
getOriginalImageByDocumentId,
|
||||
downloadFiles,
|
||||
|
||||
@@ -158,6 +158,22 @@ describe("payroll payout helpers", () => {
|
||||
)
|
||||
).toThrow("Missing commission percent for Jane Doe on labor type LAA.");
|
||||
});
|
||||
|
||||
it("throws a useful error when an hourly payout rate is missing", () => {
|
||||
expect(() =>
|
||||
payAllModule.BuildPayoutDetails(
|
||||
{},
|
||||
{
|
||||
labor_rates: {},
|
||||
employee: {
|
||||
first_name: "John",
|
||||
last_name: "Smith"
|
||||
}
|
||||
},
|
||||
"LAB"
|
||||
)
|
||||
).toThrow("Missing hourly payout rate for John Smith on labor type LAB.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("payroll routes", () => {
|
||||
@@ -364,4 +380,86 @@ describe("payroll routes", () => {
|
||||
error: "Task preset percentages for labor type LAA total 110% and cannot exceed 100%."
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects claim-task when an assigned team member is missing the hourly rate for the selected labor type", async () => {
|
||||
const job = buildBaseJob({
|
||||
bodyshop: {
|
||||
id: "shop-1",
|
||||
md_responsibility_centers: {
|
||||
defaults: {
|
||||
costs: {
|
||||
LAB: "Body"
|
||||
}
|
||||
}
|
||||
},
|
||||
md_tasks_presets: {
|
||||
presets: [
|
||||
{
|
||||
name: "Teardown",
|
||||
hourstype: ["LAB"],
|
||||
percent: 100,
|
||||
nextstatus: "In Progress",
|
||||
memo: "Teardown"
|
||||
}
|
||||
]
|
||||
},
|
||||
employee_teams: [
|
||||
{
|
||||
id: "team-1",
|
||||
employee_team_members: [
|
||||
{
|
||||
percentage: 50,
|
||||
labor_rates: {
|
||||
LAB: 45
|
||||
},
|
||||
employee: {
|
||||
id: "emp-1",
|
||||
first_name: "Configured",
|
||||
last_name: "Tech"
|
||||
}
|
||||
},
|
||||
{
|
||||
percentage: 50,
|
||||
labor_rates: {},
|
||||
employee: {
|
||||
id: "emp-2",
|
||||
first_name: "Missing",
|
||||
last_name: "Rate"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
joblines: [
|
||||
{
|
||||
mod_lbr_ty: "LAB",
|
||||
mod_lb_hrs: 4.4,
|
||||
assigned_team: "team-1",
|
||||
convertedtolbr: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { client, req, res } = buildReqRes({
|
||||
job,
|
||||
body: {
|
||||
task: "Teardown",
|
||||
calculateOnly: true,
|
||||
employee: {
|
||||
name: "Dave",
|
||||
email: "dave@rome.test"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await claimTaskModule.claimtask(req, res);
|
||||
|
||||
expect(client.request).toHaveBeenCalledTimes(1);
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Missing hourly payout rate for Missing Rate on labor type LAB."
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||
const { newEsignDocument, distributeDocument, deleteDocument } = require("../esign/esign-new");
|
||||
const { esignWebhook } = require("../esign/webhook");
|
||||
|
||||
//router.use(validateFirebaseIdTokenMiddleware);
|
||||
|
||||
router.post("/new", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, newEsignDocument);
|
||||
router.post("/distribute", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, distributeDocument);
|
||||
router.post("/delete", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, deleteDocument);
|
||||
router.post("/webhook", esignWebhook);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,9 +2,6 @@ exports.servertime = (req, res) => {
|
||||
res.status(200).send(new Date());
|
||||
};
|
||||
|
||||
exports.jsrAuthString =() => {
|
||||
return "Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64")
|
||||
}
|
||||
exports.jsrAuth = async (req, res) => {
|
||||
res.send(exports.jsrAuthString());
|
||||
res.send("Basic " + Buffer.from(`${process.env.JSR_USER}:${process.env.JSR_PASSWORD}`).toString("base64"));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user