Compare commits
92 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcba77fe20 | ||
|
|
e0f55b8e7a | ||
|
|
ef6aee0518 | ||
|
|
88ae1fb1cc | ||
|
|
c6af2b34b2 | ||
|
|
d51dcc0ef2 | ||
|
|
e6178a613d | ||
|
|
2a69115903 | ||
|
|
c8262da440 | ||
|
|
1f41a532e2 | ||
|
|
32e67b14b6 | ||
|
|
d901004751 | ||
|
|
661e019a4d | ||
|
|
cd054fcf33 | ||
|
|
5ab54433ff | ||
|
|
62c053ed87 | ||
|
|
6242e0f309 | ||
|
|
614420d7d2 | ||
|
|
3113818a91 | ||
|
|
92a3e57205 | ||
|
|
de6038038a | ||
|
|
8a043767cd | ||
|
|
1f8836d9d8 | ||
|
|
a267d65425 | ||
|
|
9267e584ff | ||
|
|
cacda3805a | ||
|
|
69861af88c | ||
|
|
d7294ebba6 | ||
|
|
d9270102b1 | ||
|
|
af757ee71e | ||
|
|
eb666f2ca1 | ||
|
|
d991e32501 | ||
|
|
2b8990950b | ||
|
|
3f2e05befc | ||
|
|
06bfdeb449 | ||
|
|
66df286ddb | ||
|
|
1b2f9fc027 | ||
|
|
1287c7ec36 | ||
|
|
fb29fa2caa | ||
|
|
6bda497d8c | ||
|
|
a018b6dc5a | ||
|
|
8a4679f86c | ||
|
|
4d558da46a | ||
|
|
90789e743f | ||
|
|
a4dbc5250e | ||
|
|
704543d823 | ||
|
|
d8924d6cf3 | ||
|
|
a1d0e2df93 | ||
|
|
9a86a337bb | ||
|
|
fe848b5de4 | ||
|
|
a287601f27 | ||
|
|
7688f22161 | ||
|
|
2cc6774334 | ||
|
|
efdcd06921 | ||
|
|
d2dd276ce7 | ||
|
|
c0a37d7c1a | ||
|
|
6947ad54a7 | ||
|
|
6759bc5865 | ||
|
|
db52bf0e94 | ||
|
|
04732fc6cd | ||
|
|
5d95275c0b | ||
|
|
a65a34ef1f | ||
|
|
1ea7798eeb | ||
|
|
7739d48741 | ||
|
|
ed0693fc5b | ||
|
|
074be66b8c | ||
|
|
8db8744782 | ||
|
|
c2d8d78e0a | ||
|
|
e9e189d032 | ||
|
|
71aec6d0c5 | ||
|
|
f89d7865fa | ||
|
|
8fd368ebb4 | ||
|
|
132fc0a20f | ||
|
|
0b470e3c31 | ||
|
|
ab8b44bee4 | ||
|
|
9ea2d83043 | ||
|
|
abad7d5f00 | ||
|
|
d497ec9f7d | ||
|
|
e49500887d | ||
|
|
b8246e03c1 | ||
|
|
cc623b7cbb | ||
|
|
3aa19ec09f | ||
|
|
866e9581c2 | ||
|
|
1102670e66 | ||
|
|
591439b79c | ||
|
|
2de605e520 | ||
|
|
2690e09626 | ||
|
|
dd306e1a7b | ||
|
|
fd712da4a3 | ||
|
|
bcb693f03c | ||
|
|
c33a3118bc | ||
|
|
d23a182650 |
586
client/package-lock.json
generated
586
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/analytics-browser": "^2.37.0",
|
"@amplitude/analytics-browser": "^2.38.0",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.6",
|
"@apollo/client": "^4.1.6",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -24,29 +24,29 @@
|
|||||||
"@firebase/messaging": "^0.12.25",
|
"@firebase/messaging": "^0.12.25",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@sentry/cli": "^3.3.3",
|
"@sentry/cli": "^3.3.5",
|
||||||
"@sentry/react": "^10.45.0",
|
"@sentry/react": "^10.47.0",
|
||||||
"@sentry/vite-plugin": "^4.9.1",
|
"@sentry/vite-plugin": "^4.9.1",
|
||||||
"@splitsoftware/splitio-react": "^2.6.1",
|
"@splitsoftware/splitio-react": "^2.6.1",
|
||||||
"@tanem/react-nprogress": "^5.0.63",
|
"@tanem/react-nprogress": "^5.0.63",
|
||||||
"antd": "^6.3.3",
|
"antd": "^6.3.5",
|
||||||
"apollo-link-logger": "^3.0.0",
|
"apollo-link-logger": "^3.0.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.14.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"dayjs-business-days2": "^1.3.2",
|
"dayjs-business-days2": "^1.3.3",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"env-cmd": "^11.0.0",
|
"env-cmd": "^11.0.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.13.1",
|
"graphql": "^16.13.2",
|
||||||
"graphql-ws": "^6.0.7",
|
"graphql-ws": "^6.0.8",
|
||||||
"i18next": "^25.10.5",
|
"i18next": "^25.10.10",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.40",
|
"libphonenumber-js": "^1.12.41",
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"logrocket": "^12.1.0",
|
"logrocket": "^12.1.0",
|
||||||
"markerjs2": "^2.32.7",
|
"markerjs2": "^2.32.7",
|
||||||
@@ -54,18 +54,18 @@
|
|||||||
"normalize-url": "^8.1.1",
|
"normalize-url": "^8.1.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"posthog-js": "^1.363.2",
|
"posthog-js": "^1.364.4",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-big-calendar": "^1.19.4",
|
"react-big-calendar": "^1.19.4",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.1.0",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.3",
|
||||||
"react-i18next": "^16.6.2",
|
"react-i18next": "^16.6.6",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"react-router-dom": "^7.13.2",
|
"react-router-dom": "^7.13.2",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.18.3",
|
"react-virtuoso": "^4.18.3",
|
||||||
"recharts": "^3.8.0",
|
"recharts": "^3.8.1",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"styled-components": "^6.3.12",
|
"styled-components": "^6.3.12",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||||
@@ -137,10 +137,10 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.28.5",
|
||||||
"@dotenvx/dotenvx": "^1.57.2",
|
"@dotenvx/dotenvx": "^1.59.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"browserslist": "^4.28.1",
|
"browserslist": "^4.28.2",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
@@ -167,10 +167,10 @@
|
|||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-babel": "^1.6.0",
|
"vite-plugin-babel": "^1.6.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.25.0",
|
"vite-plugin-node-polyfills": "^0.26.0",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.2.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^4.1.0",
|
"vitest": "^4.1.2",
|
||||||
"workbox-window": "^7.4.0"
|
"workbox-window": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Alert } from "antd";
|
import { Alert } from "antd";
|
||||||
|
|
||||||
export default function AlertComponent(props) {
|
export default function AlertComponent({ title, message, ...props }) {
|
||||||
return <Alert {...props} />;
|
return <Alert {...props} title={title ?? message} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import { buildBillUpdateAuditDetails } from "../../utils/auditTrailDetails";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||||
@@ -134,10 +135,16 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
|
|||||||
|
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
|
|
||||||
|
const details = buildBillUpdateAuditDetails({
|
||||||
|
originalBill: data?.bills_by_pk,
|
||||||
|
bill,
|
||||||
|
billlines
|
||||||
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: bill.jobid,
|
jobid: bill.jobid ?? data?.bills_by_pk?.jobid,
|
||||||
billid: search.billid,
|
billid: search.billid,
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
operation: AuditTrailMapping.billupdated(bill.invoice_number, details),
|
||||||
type: "billupdated"
|
type: "billupdated"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function BillFormComponent({
|
|||||||
const [discount, setDiscount] = useState(0);
|
const [discount, setDiscount] = useState(0);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const jobIdFormWatch = Form.useWatch("jobid", form);
|
const jobIdFormWatch = Form.useWatch("jobid", form);
|
||||||
|
const vendorIdFormWatch = Form.useWatch("vendorid", form);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Extended_Bill_Posting, ClosingPeriod }
|
treatments: { Extended_Bill_Posting, ClosingPeriod }
|
||||||
@@ -118,6 +119,7 @@ export function BillFormComponent({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
form,
|
form,
|
||||||
|
vendorIdFormWatch,
|
||||||
billEdit,
|
billEdit,
|
||||||
loadOutstandingReturns,
|
loadOutstandingReturns,
|
||||||
loadInventory,
|
loadInventory,
|
||||||
|
|||||||
@@ -9,18 +9,20 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel
|
authLevel: selectAuthLevel
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = () => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
|
||||||
|
|
||||||
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
export function BillMarkForReexportButton({ bodyshop, authLevel, bill, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
@@ -47,6 +49,12 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("bills.successes.reexport")
|
title: t("bills.successes.reexport")
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: bill.jobid,
|
||||||
|
billid: bill.id,
|
||||||
|
operation: AuditTrailMapping.billmarkforreexport(bill.invoice_number),
|
||||||
|
type: "billmarkforreexport"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("bills.errors.saving", {
|
title: t("bills.errors.saving", {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -156,104 +157,127 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
|
|||||||
joblines: {
|
joblines: {
|
||||||
data: billingLines
|
data: billingLines
|
||||||
},
|
},
|
||||||
parts_tax_rates: {
|
...InstanceRenderManager({
|
||||||
PAA: {
|
imex: {
|
||||||
prt_type: "PAA",
|
parts_tax_rates: {
|
||||||
prt_discp: 0,
|
PAA: {
|
||||||
prt_mktyp: false,
|
prt_type: "PAA",
|
||||||
prt_mkupp: 0,
|
prt_discp: 0,
|
||||||
prt_tax_in: true,
|
prt_mktyp: false,
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAC: {
|
||||||
|
prt_type: "PAC",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAL: {
|
||||||
|
prt_type: "PAL",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAM: {
|
||||||
|
prt_type: "PAM",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAN: {
|
||||||
|
prt_type: "PAN",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAR: {
|
||||||
|
prt_type: "PAR",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
PAS: {
|
||||||
|
prt_type: "PAS",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
CCDR: {
|
||||||
|
prt_type: "CCDR",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
CCF: {
|
||||||
|
prt_type: "CCF",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
CCM: {
|
||||||
|
prt_type: "CCM",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
CCC: {
|
||||||
|
prt_type: "CCC",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
},
|
||||||
|
CCD: {
|
||||||
|
prt_type: "CCD",
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: false,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_tax_in: true,
|
||||||
|
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
PAC: {
|
rome: {
|
||||||
prt_type: "PAC",
|
cieca_pft: {
|
||||||
prt_discp: 0,
|
...bodyshop.md_responsibility_centers.taxes.tax_ty1,
|
||||||
prt_mktyp: false,
|
...bodyshop.md_responsibility_centers.taxes.tax_ty2,
|
||||||
prt_mkupp: 0,
|
...bodyshop.md_responsibility_centers.taxes.tax_ty3,
|
||||||
prt_tax_in: true,
|
...bodyshop.md_responsibility_centers.taxes.tax_ty4,
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
...bodyshop.md_responsibility_centers.taxes.tax_ty5
|
||||||
},
|
},
|
||||||
PAL: {
|
materials: bodyshop.md_responsibility_centers.cieca_pfm,
|
||||||
prt_type: "PAL",
|
cieca_pfl: bodyshop.md_responsibility_centers.cieca_pfl,
|
||||||
prt_discp: 0,
|
parts_tax_rates: bodyshop.md_responsibility_centers.parts_tax_rates,
|
||||||
prt_mktyp: false,
|
tax_tow_rt: bodyshop.md_responsibility_centers.tax_tow_rt,
|
||||||
prt_mkupp: 0,
|
tax_str_rt: bodyshop.md_responsibility_centers.tax_str_rt,
|
||||||
prt_tax_in: true,
|
tax_paint_mat_rt: bodyshop.md_responsibility_centers.tax_paint_mat_rt,
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
tax_shop_mat_rt: bodyshop.md_responsibility_centers.tax_shop_mat_rt,
|
||||||
},
|
tax_sub_rt: bodyshop.md_responsibility_centers.tax_sub_rt,
|
||||||
PAM: {
|
tax_lbr_rt: bodyshop.md_responsibility_centers.tax_lbr_rt,
|
||||||
prt_type: "PAM",
|
tax_levies_rt: bodyshop.md_responsibility_centers.tax_levies_rt
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
PAN: {
|
|
||||||
prt_type: "PAN",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
PAR: {
|
|
||||||
prt_type: "PAR",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
PAS: {
|
|
||||||
prt_type: "PAS",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
CCDR: {
|
|
||||||
prt_type: "CCDR",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
CCF: {
|
|
||||||
prt_type: "CCF",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
CCM: {
|
|
||||||
prt_type: "CCM",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
CCC: {
|
|
||||||
prt_type: "CCC",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
},
|
|
||||||
CCD: {
|
|
||||||
prt_type: "CCD",
|
|
||||||
prt_discp: 0,
|
|
||||||
prt_mktyp: false,
|
|
||||||
prt_mkupp: 0,
|
|
||||||
prt_tax_in: true,
|
|
||||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentUser?.email) {
|
if (currentUser?.email) {
|
||||||
@@ -287,7 +311,7 @@ export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("jobs.successes.created"),
|
title: t("jobs.successes.created"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push(`/manage/jobs/${result.data.insert_jobs.returning[0].id}`);
|
history(`/manage/jobs/${result.data.insert_jobs.returning[0].id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function normalizeJobAllocations(ack) {
|
|||||||
* RR-specific DMS Allocations Summary
|
* RR-specific DMS Allocations Summary
|
||||||
* Focused on what we actually send to RR:
|
* Focused on what we actually send to RR:
|
||||||
* - ROGOG (split by taxable / non-taxable segments)
|
* - ROGOG (split by taxable / non-taxable segments)
|
||||||
* - ROLABOR shell
|
* - ROLABOR labor rows with bill hours / rates
|
||||||
*
|
*
|
||||||
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
|
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
|
||||||
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
|
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
|
||||||
@@ -181,21 +181,30 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
|||||||
const rolaborRows = useMemo(() => {
|
const rolaborRows = useMemo(() => {
|
||||||
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
|
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
|
||||||
|
|
||||||
return rolaborPreview.ops.map((op, idx) => {
|
return rolaborPreview.ops
|
||||||
const rowOpCode = opCode || op.opCode;
|
.filter((op) =>
|
||||||
|
[op.bill?.jobTotalHrs, op.bill?.billTime, op.bill?.billRate, op.amount?.custPrice, op.amount?.totalAmt]
|
||||||
|
.map((value) => Number.parseFloat(value ?? "0"))
|
||||||
|
.some((value) => !Number.isNaN(value) && value !== 0)
|
||||||
|
)
|
||||||
|
.map((op, idx) => {
|
||||||
|
const rowOpCode = opCode || op.opCode;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: `${op.jobNo}-${idx}`,
|
key: `${op.jobNo}-${idx}`,
|
||||||
opCode: rowOpCode,
|
opCode: rowOpCode,
|
||||||
jobNo: op.jobNo,
|
jobNo: op.jobNo,
|
||||||
custPayTypeFlag: op.custPayTypeFlag,
|
custPayTypeFlag: op.custPayTypeFlag,
|
||||||
custTxblNtxblFlag: op.custTxblNtxblFlag,
|
custTxblNtxblFlag: op.custTxblNtxblFlag,
|
||||||
payType: op.bill?.payType,
|
payType: op.bill?.payType,
|
||||||
amtType: op.amount?.amtType,
|
jobTotalHrs: op.bill?.jobTotalHrs,
|
||||||
custPrice: op.amount?.custPrice,
|
billTime: op.bill?.billTime,
|
||||||
totalAmt: op.amount?.totalAmt
|
billRate: op.bill?.billRate,
|
||||||
};
|
amtType: op.amount?.amtType,
|
||||||
});
|
custPrice: op.amount?.custPrice,
|
||||||
|
totalAmt: op.amount?.totalAmt
|
||||||
|
};
|
||||||
|
});
|
||||||
}, [rolaborPreview, opCode]);
|
}, [rolaborPreview, opCode]);
|
||||||
|
|
||||||
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
|
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
|
||||||
@@ -245,6 +254,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
|||||||
{ title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" },
|
{ title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" },
|
||||||
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
|
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
|
||||||
{ title: "PayType", dataIndex: "payType", key: "payType" },
|
{ title: "PayType", dataIndex: "payType", key: "payType" },
|
||||||
|
{ title: "JobTotalHrs", dataIndex: "jobTotalHrs", key: "jobTotalHrs" },
|
||||||
|
{ title: "BillTime", dataIndex: "billTime", key: "billTime" },
|
||||||
|
{ title: "BillRate", dataIndex: "billRate", key: "billRate" },
|
||||||
{ title: "AmtType", dataIndex: "amtType", key: "amtType" },
|
{ title: "AmtType", dataIndex: "amtType", key: "amtType" },
|
||||||
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
|
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
|
||||||
{ title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" }
|
{ title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" }
|
||||||
@@ -317,12 +329,13 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
|||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||||
This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
|
This mirrors the labor rows RR will receive, including weighted bill hours and rates derived from the
|
||||||
|
job's labor lines.
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<ResponsiveTable
|
<ResponsiveTable
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={rolaborColumns}
|
columns={rolaborColumns}
|
||||||
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
|
mobileColumnKeys={["jobNo", "opCode", "billRate", "custPrice"]}
|
||||||
rowKey="key"
|
rowKey="key"
|
||||||
dataSource={rolaborRows}
|
dataSource={rolaborRows}
|
||||||
locale={{ emptyText: "No ROLABOR lines would be generated." }}
|
locale={{ emptyText: "No ROLABOR lines would be generated." }}
|
||||||
|
|||||||
@@ -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,20 +4,203 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
import "./form-fields-changed.styles.scss";
|
import "./form-fields-changed.styles.scss";
|
||||||
import Prompt from "../../utils/prompt";
|
import Prompt from "../../utils/prompt";
|
||||||
|
|
||||||
export default function FormsFieldChanged({ form, skipPrompt }) {
|
export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const normalizeNamePath = (namePath) => (Array.isArray(namePath) ? namePath.filter((part) => part !== undefined) : [namePath]);
|
||||||
|
|
||||||
|
const getFieldIdCandidates = (namePath) => {
|
||||||
|
const normalizedNamePath = normalizeNamePath(namePath).map((part) => String(part));
|
||||||
|
const underscoreId = normalizedNamePath.join("_");
|
||||||
|
const dashId = normalizedNamePath.join("-");
|
||||||
|
const dotName = normalizedNamePath.join(".");
|
||||||
|
|
||||||
|
return [underscoreId, dashId, dotName].filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFormMeta = () => {
|
||||||
|
const fieldMeta = form.getFieldsError().map(({ name }) => ({
|
||||||
|
name,
|
||||||
|
touched: false,
|
||||||
|
validating: false,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (fieldMeta.length > 0) {
|
||||||
|
form.setFields(fieldMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDirtyChange?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.resetFields();
|
if (onReset) {
|
||||||
|
onReset();
|
||||||
|
} else {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
clearFormMeta();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldDomNode = (namePath) => {
|
||||||
|
const fieldInstance = form.getFieldInstance?.(namePath);
|
||||||
|
const fieldIdCandidates = getFieldIdCandidates(namePath);
|
||||||
|
const domCandidates = [
|
||||||
|
fieldInstance?.nativeElement,
|
||||||
|
fieldInstance?.input,
|
||||||
|
fieldInstance?.resizableTextArea?.textArea,
|
||||||
|
fieldInstance
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldIdCandidates.forEach((fieldId) => {
|
||||||
|
const escapedFieldId = CSS.escape(fieldId);
|
||||||
|
const directNode = document.getElementById(fieldId) || document.querySelector(`#${escapedFieldId}`);
|
||||||
|
const labelNode = document.querySelector(`label[for="${escapedFieldId}"]`);
|
||||||
|
const namedNode = document.querySelector(`[name="${escapedFieldId}"]`);
|
||||||
|
const formItemNode =
|
||||||
|
directNode?.closest?.(".ant-form-item") ||
|
||||||
|
labelNode?.closest?.(".ant-form-item") ||
|
||||||
|
namedNode?.closest?.(".ant-form-item");
|
||||||
|
|
||||||
|
domCandidates.push(directNode);
|
||||||
|
domCandidates.push(namedNode);
|
||||||
|
domCandidates.push(formItemNode);
|
||||||
|
domCandidates.push(formItemNode?.querySelector?.("input, textarea, select, .ant-select-selector"));
|
||||||
|
});
|
||||||
|
|
||||||
|
return domCandidates.find((candidate) => candidate instanceof HTMLElement) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForAnimationFrames = (frameCount = 1) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
let remainingFrames = frameCount;
|
||||||
|
const nextFrame = () => {
|
||||||
|
if (remainingFrames <= 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
remainingFrames -= 1;
|
||||||
|
window.requestAnimationFrame(nextFrame);
|
||||||
|
};
|
||||||
|
window.requestAnimationFrame(nextFrame);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getFieldOwningTabMeta = (namePath) => {
|
||||||
|
const fieldDomNode = getFieldDomNode(namePath);
|
||||||
|
const owningTabPane = fieldDomNode?.closest?.(".ant-tabs-tabpane");
|
||||||
|
const paneId = owningTabPane?.getAttribute?.("id") || null;
|
||||||
|
const owningTabButton = paneId
|
||||||
|
? document.querySelector(`[role="tab"][aria-controls="${paneId.replace(/"/g, '\\"')}"]`)
|
||||||
|
: null;
|
||||||
|
const tabLabel = owningTabButton?.textContent?.trim() || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
owningTabPane,
|
||||||
|
owningTabButton,
|
||||||
|
tabLabel
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFieldOwningTab = async (namePath) => {
|
||||||
|
const { owningTabPane, owningTabButton } = getFieldOwningTabMeta(namePath);
|
||||||
|
if (!owningTabPane || owningTabPane.classList.contains("ant-tabs-tabpane-active")) return false;
|
||||||
|
|
||||||
|
if (!(owningTabButton instanceof HTMLElement)) return false;
|
||||||
|
|
||||||
|
owningTabButton.click();
|
||||||
|
|
||||||
|
for (let index = 0; index < 24; index += 1) {
|
||||||
|
await waitForAnimationFrames();
|
||||||
|
if (owningTabPane.classList.contains("ant-tabs-tabpane-active")) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return owningTabPane.classList.contains("ant-tabs-tabpane-active");
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToErrorField = (namePath) => {
|
||||||
|
const normalizedNamePath = normalizeNamePath(namePath);
|
||||||
|
if (!normalizedNamePath.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
form.scrollToField(normalizedNamePath, {
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
focus: true
|
||||||
|
});
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
const fallbackNode = getFieldDomNode(normalizedNamePath);
|
||||||
|
fallbackNode?.focus?.();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
const fallbackTarget = document.getElementById(normalizedNamePath[0]?.toString?.() ?? "");
|
||||||
|
fallbackTarget?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorClick = async (namePath) => {
|
||||||
|
const normalizedNamePath = normalizeNamePath(namePath);
|
||||||
|
if (!normalizedNamePath.length) return;
|
||||||
|
|
||||||
|
const switchedTab = await openFieldOwningTab(normalizedNamePath);
|
||||||
|
if (!switchedTab) {
|
||||||
|
const navigationDelayMs = onErrorNavigate?.(normalizedNamePath) ?? 0;
|
||||||
|
if (navigationDelayMs > 0) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
scrollToErrorField(normalizedNamePath);
|
||||||
|
});
|
||||||
|
}, navigationDelayMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForAnimationFrames(switchedTab ? 2 : 1);
|
||||||
|
scrollToErrorField(normalizedNamePath);
|
||||||
};
|
};
|
||||||
//if (!form.isFieldsTouched()) return <></>;
|
//if (!form.isFieldsTouched()) return <></>;
|
||||||
return (
|
return (
|
||||||
<Form.Item className="form-fields-changed" shouldUpdate style={{ margin: 0, padding: 0, minHeight: "unset" }}>
|
<Form.Item className="form-fields-changed" shouldUpdate style={{ margin: 0, padding: 0, minHeight: "unset" }}>
|
||||||
{() => {
|
{() => {
|
||||||
const errors = form.getFieldsError().filter((e) => e.errors.length > 0);
|
const errors = form
|
||||||
|
.getFieldsError()
|
||||||
|
.filter((fieldError) => fieldError.errors.length > 0)
|
||||||
|
.flatMap((fieldError) => {
|
||||||
|
const tabMeta = getFieldOwningTabMeta(fieldError.name);
|
||||||
|
|
||||||
|
return fieldError.errors.map((errorMessage, errorIndex) => ({
|
||||||
|
key: `${(fieldError.name || []).join(".")}-${errorIndex}-${errorMessage}`,
|
||||||
|
message: errorMessage,
|
||||||
|
namePath: fieldError.name,
|
||||||
|
tabLabel: tabMeta.tabLabel
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedErrors = errors.reduce((groups, error) => {
|
||||||
|
const groupKey = error.tabLabel || "__ungrouped__";
|
||||||
|
if (!groups[groupKey]) {
|
||||||
|
groups[groupKey] = {
|
||||||
|
key: groupKey,
|
||||||
|
label: error.tabLabel,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
groups[groupKey].errors.push(error);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
const errorGroups = Object.values(groupedErrors);
|
||||||
|
const hasTabbedErrorGroups = errorGroups.some((group) => Boolean(group.label));
|
||||||
|
|
||||||
if (form.isFieldsTouched())
|
if (form.isFieldsTouched())
|
||||||
return (
|
return (
|
||||||
<Space orientation="vertical" style={{ width: "100%" }}>
|
<Space orientation="vertical" style={{ width: "100%", marginBottom: 10 }}>
|
||||||
<Prompt when={!skipPrompt} beforeUnload={true} message={t("general.messages.unsavedchangespopup")} />
|
<Prompt when={!skipPrompt} beforeUnload={true} message={t("general.messages.unsavedchangespopup")} />
|
||||||
<AlertComponent
|
<AlertComponent
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -39,10 +222,35 @@ export default function FormsFieldChanged({ form, skipPrompt }) {
|
|||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<AlertComponent
|
<AlertComponent
|
||||||
type="error"
|
type="error"
|
||||||
message={t("general.labels.validationerror")}
|
title={t("general.labels.validationerror")}
|
||||||
description={
|
description={
|
||||||
<div>
|
<div className="form-fields-changed__error-groups">
|
||||||
<ul>{errors.map((e, idx) => e.errors.map((e2, idx2) => <li key={`${idx}${idx2}`}>{e2}</li>))}</ul>
|
{errorGroups.map((group) => (
|
||||||
|
<div key={group.key} className="form-fields-changed__error-group">
|
||||||
|
{hasTabbedErrorGroups && group.label ? (
|
||||||
|
<div className="form-fields-changed__error-group-title">{group.label}</div>
|
||||||
|
) : null}
|
||||||
|
<ul className="form-fields-changed__error-list">
|
||||||
|
{group.errors.map((error) => (
|
||||||
|
<li key={error.key}>
|
||||||
|
{Array.isArray(error.namePath) && error.namePath.length > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="form-fields-changed__error-link"
|
||||||
|
onClick={() => {
|
||||||
|
handleErrorClick(error.namePath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
error.message
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
@@ -4,4 +4,47 @@
|
|||||||
min-height: unset !important;
|
min-height: unset !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__error-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-group-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-link {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: color-mix(in srgb, var(--ant-color-error) 82%, var(--ant-color-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--ant-color-error) 32%, transparent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,88 @@
|
|||||||
import { Input } from "antd";
|
import { PhoneFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Space } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
|
import { forwardRef, useMemo, useState } from "react";
|
||||||
import "./phone-form-item.styles.scss";
|
import "./phone-form-item.styles.scss";
|
||||||
|
|
||||||
function FormItemPhone({ ref, ...props }) {
|
/**
|
||||||
return <Input ref={ref} {...props} />;
|
* Formats a phone number for display purposes. If the input value is a valid phone number, it will be formatted in a
|
||||||
}
|
* national format (e.g., (123) 456-7890 for US/CA). If the input is not a valid phone number, it will be returned as-is.
|
||||||
|
* @param value
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const formatPhoneDisplayValue = (value) => {
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedPhone = parsePhoneNumber(value, "CA");
|
||||||
|
return parsedPhone?.isValid() ? parsedPhone.formatNational() : value;
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a "tel:" URL for a phone number if it's valid. If the input value is a valid phone number, it will return a
|
||||||
|
* URL in the format "tel:+1234567890". If the input is not a valid phone number, it will attempt to trim whitespace and
|
||||||
|
* return a "tel:" URL with the raw value, or null if the trimmed value is empty.
|
||||||
|
* @param value
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
const getPhoneActionHref = (value) => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedPhone = parsePhoneNumber(value, "CA");
|
||||||
|
if (parsedPhone?.isValid()) return `tel:${parsedPhone.number}`;
|
||||||
|
} catch {
|
||||||
|
// Fall back to the raw value below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedValue = String(value).trim();
|
||||||
|
return trimmedValue ? `tel:${trimmedValue}` : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemPhone = forwardRef(function FormItemPhone(
|
||||||
|
{ formatDisplayOnly = false, showPhoneAction = false, value, onBlur, onFocus, ...props },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const displayValue = useMemo(() => {
|
||||||
|
if (!formatDisplayOnly || isFocused) return value;
|
||||||
|
return formatPhoneDisplayValue(value);
|
||||||
|
}, [formatDisplayOnly, isFocused, value]);
|
||||||
|
const phoneActionHref = useMemo(() => (showPhoneAction ? getPhoneActionHref(value) : null), [showPhoneAction, value]);
|
||||||
|
|
||||||
|
const input = (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={displayValue}
|
||||||
|
onFocus={(event) => {
|
||||||
|
setIsFocused(true);
|
||||||
|
onFocus?.(event);
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
setIsFocused(false);
|
||||||
|
onBlur?.(event);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showPhoneAction) return input;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
|
{input}
|
||||||
|
{phoneActionHref ? (
|
||||||
|
<Button icon={<PhoneFilled />} href={phoneActionHref} />
|
||||||
|
) : (
|
||||||
|
<Button icon={<PhoneFilled />} disabled />
|
||||||
|
)}
|
||||||
|
</Space.Compact>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default FormItemPhone;
|
export default FormItemPhone;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { LinkOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Space } from "antd";
|
||||||
|
import { forwardRef, useMemo } from "react";
|
||||||
|
|
||||||
|
const HAS_URL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
||||||
|
const LOCALHOST_OR_IP_REGEX = /^(localhost|127(?:\.\d{1,3}){3}|\d{1,3}(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i;
|
||||||
|
|
||||||
|
const getUrlActionHref = (value) => {
|
||||||
|
const trimmedValue = String(value ?? "").trim();
|
||||||
|
if (!trimmedValue) return null;
|
||||||
|
|
||||||
|
if (HAS_URL_PROTOCOL_REGEX.test(trimmedValue)) return trimmedValue;
|
||||||
|
if (trimmedValue.startsWith("//")) return `https:${trimmedValue}`;
|
||||||
|
if (LOCALHOST_OR_IP_REGEX.test(trimmedValue)) return `http://${trimmedValue}`;
|
||||||
|
|
||||||
|
return `https://${trimmedValue}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemUrl = forwardRef(function FormItemUrl({ value, defaultValue, ...props }, ref) {
|
||||||
|
const urlActionHref = useMemo(() => getUrlActionHref(value ?? defaultValue), [defaultValue, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space.Compact style={{ width: "100%" }}>
|
||||||
|
<Input ref={ref} {...props} value={value} defaultValue={defaultValue} />
|
||||||
|
{urlActionHref ? (
|
||||||
|
<Button icon={<LinkOutlined />} href={urlActionHref} target="_blank" rel="noopener noreferrer" />
|
||||||
|
) : (
|
||||||
|
<Button icon={<LinkOutlined />} disabled />
|
||||||
|
)}
|
||||||
|
</Space.Compact>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FormItemUrl;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Normalize Form Item List Titles
|
||||||
|
* @param value
|
||||||
|
* @returns {*|string}
|
||||||
|
*/
|
||||||
|
const normalizeFormListTitleValue = (value) => {
|
||||||
|
if (value === null || value === undefined) return "";
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map((item) => normalizeFormListTitleValue(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value).trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Form Listem Item Title
|
||||||
|
* @param fallbackLabel
|
||||||
|
* @param index
|
||||||
|
* @param candidates
|
||||||
|
* @returns {*|string}
|
||||||
|
*/
|
||||||
|
export function getFormListItemTitle(fallbackLabel, index, ...candidates) {
|
||||||
|
const title = candidates.map((candidate) => normalizeFormListTitleValue(candidate)).find(Boolean);
|
||||||
|
|
||||||
|
return title || `${fallbackLabel} ${index + 1}`;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,16 +14,20 @@ import CriticalPartsScan from "../../utils/criticalPartsScan";
|
|||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
import { buildJobLineInsertAuditDetails, buildJobLineUpdateAuditDetails } from "../../utils/auditTrailDetails.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobLineEditModal: selectJobLineEditModal,
|
jobLineEditModal: selectJobLineEditModal,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop }) {
|
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||||
const {
|
const {
|
||||||
treatments: { CriticalPartsScanning }
|
treatments: { CriticalPartsScanning }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -74,6 +78,11 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.successes.created")
|
title: t("joblines.successes.created")
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobLineEditModal.context.jobid,
|
||||||
|
operation: AuditTrailMapping.jobmanuallineinsert(buildJobLineInsertAuditDetails(values)),
|
||||||
|
type: "jobmanuallineinsert"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("joblines.errors.creating", {
|
title: t("joblines.errors.creating", {
|
||||||
@@ -103,6 +112,17 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.successes.updated")
|
title: t("joblines.successes.updated")
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobLineEditModal.context.jobid,
|
||||||
|
operation: AuditTrailMapping.joblineupdate(
|
||||||
|
values.line_desc || jobLineEditModal.context.line_desc || "manual line",
|
||||||
|
buildJobLineUpdateAuditDetails({
|
||||||
|
originalLine: jobLineEditModal.context,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
),
|
||||||
|
type: "joblineupdate"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.errors.updating", {
|
title: t("joblines.errors.updating", {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import ConfigListEmptyState from "./config-list-empty-state.component.jsx";
|
||||||
|
|
||||||
|
export const buildConfigListActionButton = ({ key, label, onClick, id }) => (
|
||||||
|
<Button key={key} type="primary" block id={id} onClick={onClick}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const renderConfigListOrEmpty = ({ fields, actionLabel, renderItems }) =>
|
||||||
|
fields.length === 0 ? <ConfigListEmptyState actionLabel={actionLabel} /> : renderItems();
|
||||||
|
|
||||||
|
export const buildSectionActionButton = (key, label, onClick, id) =>
|
||||||
|
buildConfigListActionButton({ key, label, onClick, id });
|
||||||
|
|
||||||
|
export const renderListOrEmpty = (fields, actionLabel, renderItems) =>
|
||||||
|
renderConfigListOrEmpty({ fields, actionLabel, renderItems });
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function ConfigListEmptyState({ actionLabel, minHeight = 96 }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="imex-form-row-empty-state" style={{ minHeight }}>
|
||||||
|
{t("general.labels.click_to_begin", { action: actionLabel })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { UnorderedListOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export const inlineFormRowTitleStyles = Object.freeze({
|
||||||
|
input: Object.freeze({
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: 0,
|
||||||
|
boxShadow: "none",
|
||||||
|
paddingInline: 0,
|
||||||
|
paddingBlock: 0,
|
||||||
|
lineHeight: 1.35,
|
||||||
|
flex: "1 1 auto",
|
||||||
|
minWidth: 0,
|
||||||
|
width: "100%"
|
||||||
|
}),
|
||||||
|
row: Object.freeze({
|
||||||
|
display: "flex",
|
||||||
|
gap: 6,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
paddingInline: 4
|
||||||
|
}),
|
||||||
|
group: Object.freeze({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
paddingInline: 8,
|
||||||
|
paddingBlock: 4,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: "1px solid var(--imex-form-title-group-border)",
|
||||||
|
background: "var(--imex-form-title-group-bg)",
|
||||||
|
minWidth: 0,
|
||||||
|
flex: "1 1 0"
|
||||||
|
}),
|
||||||
|
label: Object.freeze({
|
||||||
|
color: "var(--ant-color-text-secondary)",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: 1,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
paddingInline: 6,
|
||||||
|
paddingBlock: 3,
|
||||||
|
borderRadius: 999,
|
||||||
|
border: "1px solid var(--imex-form-title-label-border)",
|
||||||
|
background: "var(--imex-form-title-label-bg)"
|
||||||
|
}),
|
||||||
|
handle: Object.freeze({
|
||||||
|
color: "var(--ant-color-text-tertiary)",
|
||||||
|
fontSize: 14,
|
||||||
|
flex: "0 0 auto",
|
||||||
|
marginRight: 2
|
||||||
|
}),
|
||||||
|
separator: Object.freeze({
|
||||||
|
width: 1,
|
||||||
|
height: 16,
|
||||||
|
background: "color-mix(in srgb, var(--imex-form-surface-border) 58%, transparent)",
|
||||||
|
borderRadius: 999,
|
||||||
|
flex: "0 0 auto",
|
||||||
|
marginInline: 2
|
||||||
|
}),
|
||||||
|
text: Object.freeze({
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: "var(--ant-font-size-lg)",
|
||||||
|
lineHeight: 1.2
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input;
|
||||||
|
export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row;
|
||||||
|
export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group;
|
||||||
|
export const InlineTitleListIcon = UnorderedListOutlined;
|
||||||
|
export const INLINE_TITLE_SWITCH_GROUP_STYLE = Object.freeze({
|
||||||
|
...inlineFormRowTitleStyles.group,
|
||||||
|
flex: "0 0 auto"
|
||||||
|
});
|
||||||
|
export const INLINE_TITLE_LABEL_STYLE = inlineFormRowTitleStyles.label;
|
||||||
|
export const INLINE_TITLE_HANDLE_STYLE = inlineFormRowTitleStyles.handle;
|
||||||
|
export const INLINE_TITLE_SEPARATOR_STYLE = inlineFormRowTitleStyles.separator;
|
||||||
|
export const INLINE_TITLE_TEXT_STYLE = inlineFormRowTitleStyles.text;
|
||||||
|
|
||||||
|
export const INLINE_FORM_ROW_WRAP_TITLE_STYLES = Object.freeze({
|
||||||
|
title: Object.freeze({
|
||||||
|
whiteSpace: "normal",
|
||||||
|
overflow: "visible",
|
||||||
|
textOverflow: "unset"
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Form } from "antd";
|
||||||
|
import LayoutFormRow from "./layout-form-row.component";
|
||||||
|
|
||||||
|
export default function InlineValidatedFormRow({ actions, errorNames = [], extraErrors = [], form, ...layoutFormRowProps }) {
|
||||||
|
const normalizedErrorNames = Array.isArray(errorNames) ? errorNames : [errorNames];
|
||||||
|
const normalizedExtraErrors = Array.isArray(extraErrors) ? extraErrors.filter(Boolean) : [extraErrors].filter(Boolean);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
const fieldErrors = normalizedErrorNames.flatMap((name) => form?.getFieldError?.(name) || []);
|
||||||
|
const errors = [...new Set([...fieldErrors, ...normalizedExtraErrors])];
|
||||||
|
const resolvedClassName = [
|
||||||
|
layoutFormRowProps.className,
|
||||||
|
errors.length > 0 ? "imex-form-row--error" : null
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
const normalizedActions = Array.isArray(actions) ? actions.filter(Boolean) : [actions].filter(Boolean);
|
||||||
|
const resolvedActions =
|
||||||
|
errors.length > 0
|
||||||
|
? [
|
||||||
|
<div
|
||||||
|
key="inline-form-row-footer"
|
||||||
|
className="imex-inline-form-row-errors"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: normalizedActions.length > 0 ? 8 : 0,
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.ErrorList errors={errors} />
|
||||||
|
{normalizedActions.length > 0 ? <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>{normalizedActions}</div> : null}
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
: normalizedActions.length > 0
|
||||||
|
? normalizedActions
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return <LayoutFormRow {...layoutFormRowProps} className={resolvedClassName} actions={resolvedActions} />;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Card, Col, Row } from "antd";
|
import { Card, Col, Row } from "antd";
|
||||||
import { Children, isValidElement } from "react";
|
import { Children, isValidElement } from "react";
|
||||||
|
import { INLINE_FORM_ROW_WRAP_TITLE_STYLES } from "./inline-form-row-title.utils.js";
|
||||||
import "./layout-form-row.styles.scss";
|
import "./layout-form-row.styles.scss";
|
||||||
|
|
||||||
export default function LayoutFormRow({
|
export default function LayoutFormRow({
|
||||||
@@ -7,32 +8,45 @@ export default function LayoutFormRow({
|
|||||||
children,
|
children,
|
||||||
grow = false,
|
grow = false,
|
||||||
noDivider = false,
|
noDivider = false,
|
||||||
gutter = [16, 16], // Responsive gutter: horizontal, vertical
|
titleOnly = false,
|
||||||
|
wrapTitle = false,
|
||||||
|
gutter,
|
||||||
rowProps,
|
rowProps,
|
||||||
|
|
||||||
// Optional overrides if you ever need per-section customization
|
// Optional overrides if you ever need per-section customization
|
||||||
surface = true,
|
surface = true,
|
||||||
surfaceBg,
|
surfaceBg,
|
||||||
surfaceHeaderBg,
|
surfaceHeaderBg,
|
||||||
|
surfaceBorderColor,
|
||||||
|
|
||||||
...cardProps
|
...cardProps
|
||||||
}) {
|
}) {
|
||||||
const items = Children.toArray(children).filter(Boolean);
|
const items = Children.toArray(children).filter(Boolean);
|
||||||
if (items.length === 0) return null;
|
const isCompactRow = noDivider;
|
||||||
|
|
||||||
const title = !noDivider && header ? header : undefined;
|
const title = !noDivider && header ? header : undefined;
|
||||||
|
const resolvedTitle = cardProps.title ?? title;
|
||||||
|
const isHeaderOnly = titleOnly || items.length === 0;
|
||||||
|
const hideBody = isHeaderOnly;
|
||||||
|
|
||||||
|
if (items.length === 0 && !resolvedTitle) return null;
|
||||||
|
const resolvedGutter = gutter ?? [16, isCompactRow ? 8 : 16];
|
||||||
|
|
||||||
const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined);
|
const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined);
|
||||||
const headBg = surfaceHeaderBg ?? (surface ? "var(--imex-form-surface-head)" : undefined);
|
const headBg = surfaceHeaderBg ?? (surface ? "var(--imex-form-surface-head)" : undefined);
|
||||||
|
const borderColor = surfaceBorderColor ?? (surface ? "var(--imex-form-surface-border)" : undefined);
|
||||||
|
|
||||||
const mergedStyles = mergeSemanticStyles(
|
const mergedStyles = mergeSemanticStyles(
|
||||||
{
|
{
|
||||||
|
...(wrapTitle ? INLINE_FORM_ROW_WRAP_TITLE_STYLES : null),
|
||||||
header: {
|
header: {
|
||||||
paddingInline: 16,
|
paddingInline: isHeaderOnly ? 8 : isCompactRow ? 12 : 16,
|
||||||
background: headBg
|
background: headBg,
|
||||||
|
borderBottomColor: borderColor
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
padding: 16,
|
padding: hideBody ? 0 : isCompactRow ? 12 : 16,
|
||||||
|
display: hideBody ? "none" : undefined,
|
||||||
background: bg
|
background: bg
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,28 +54,12 @@ export default function LayoutFormRow({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const baseCardStyle = {
|
const baseCardStyle = {
|
||||||
marginBottom: ".8rem",
|
marginBottom: isHeaderOnly ? "0" : isCompactRow ? "8px" : ".8rem",
|
||||||
...(bg ? { background: bg } : null), // ensures the “circled area” is tinted
|
...(bg ? { background: bg } : null), // ensures the “circled area” is tinted
|
||||||
|
...(borderColor ? { borderColor } : null),
|
||||||
...cardProps.style
|
...cardProps.style
|
||||||
};
|
};
|
||||||
|
|
||||||
// single child => just render it
|
|
||||||
if (items.length === 1) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
{...cardProps}
|
|
||||||
title={cardProps.title ?? title}
|
|
||||||
size={cardProps.size ?? "small"}
|
|
||||||
variant={cardProps.variant ?? "outlined"}
|
|
||||||
className={["imex-form-row", cardProps.className].filter(Boolean).join(" ")}
|
|
||||||
style={baseCardStyle}
|
|
||||||
styles={mergedStyles}
|
|
||||||
>
|
|
||||||
{items[0]}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = items.length;
|
const count = items.length;
|
||||||
|
|
||||||
// Modern responsive strategy leveraging Ant Design 6:
|
// Modern responsive strategy leveraging Ant Design 6:
|
||||||
@@ -125,20 +123,32 @@ export default function LayoutFormRow({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
{...cardProps}
|
{...cardProps}
|
||||||
title={cardProps.title ?? title}
|
title={resolvedTitle}
|
||||||
size={cardProps.size ?? "small"}
|
size={cardProps.size ?? "small"}
|
||||||
variant={cardProps.variant ?? "outlined"}
|
variant={cardProps.variant ?? "outlined"}
|
||||||
className={["imex-form-row", cardProps.className].filter(Boolean).join(" ")}
|
className={[
|
||||||
|
"imex-form-row",
|
||||||
|
isCompactRow ? "imex-form-row--compact" : null,
|
||||||
|
isHeaderOnly ? "imex-form-row--title-only" : null,
|
||||||
|
cardProps.className
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
style={baseCardStyle}
|
style={baseCardStyle}
|
||||||
styles={mergedStyles}
|
styles={mergedStyles}
|
||||||
>
|
>
|
||||||
<Row gutter={gutter} wrap {...rowProps}>
|
{!isHeaderOnly &&
|
||||||
{items.map((child, idx) => (
|
(items.length === 1 ? (
|
||||||
<Col key={child?.key ?? idx} {...getColPropsForChild(child)}>
|
items[0]
|
||||||
{child}
|
) : (
|
||||||
</Col>
|
<Row gutter={resolvedGutter} wrap {...rowProps}>
|
||||||
|
{items.map((child, idx) => (
|
||||||
|
<Col key={child?.key ?? idx} {...getColPropsForChild(child)}>
|
||||||
|
{child}
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
))}
|
))}
|
||||||
</Row>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -152,6 +162,7 @@ function mergeSemanticStyles(defaults, userStyles) {
|
|||||||
return {
|
return {
|
||||||
...defaults,
|
...defaults,
|
||||||
...computed,
|
...computed,
|
||||||
|
title: { ...(defaults.title || {}), ...(computed.title || {}) },
|
||||||
header: { ...defaults.header, ...(computed.header || {}) },
|
header: { ...defaults.header, ...(computed.header || {}) },
|
||||||
body: { ...defaults.body, ...(computed.body || {}) }
|
body: { ...defaults.body, ...(computed.body || {}) }
|
||||||
};
|
};
|
||||||
@@ -161,6 +172,7 @@ function mergeSemanticStyles(defaults, userStyles) {
|
|||||||
return {
|
return {
|
||||||
...defaults,
|
...defaults,
|
||||||
...userStyles,
|
...userStyles,
|
||||||
|
title: { ...(defaults.title || {}), ...(userStyles.title || {}) },
|
||||||
header: { ...defaults.header, ...(userStyles.header || {}) },
|
header: { ...defaults.header, ...(userStyles.header || {}) },
|
||||||
body: { ...defaults.body, ...(userStyles.body || {}) }
|
body: { ...defaults.body, ...(userStyles.body || {}) }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
--imex-form-surface: #fafafa; /* subtle contrast vs white page */
|
--imex-form-surface: #fafafa; /* subtle contrast vs white page */
|
||||||
--imex-form-surface-head: #f5f5f5; /* header strip */
|
--imex-form-surface-head: #f5f5f5; /* header strip */
|
||||||
--imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */
|
--imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */
|
||||||
|
--imex-form-title-input-bg: rgba(255, 255, 255, 0.96);
|
||||||
|
--imex-form-title-input-border: rgba(0, 0, 0, 0.08);
|
||||||
|
--imex-form-title-group-bg: rgba(255, 255, 255, 0.72);
|
||||||
|
--imex-form-title-group-border: rgba(0, 0, 0, 0.08);
|
||||||
|
--imex-form-title-label-bg: rgba(0, 0, 0, 0.04);
|
||||||
|
--imex-form-title-label-border: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pick the selector that matches your app and remove the rest */
|
/* Pick the selector that matches your app and remove the rest */
|
||||||
@@ -20,6 +26,12 @@ html[data-theme="dark"] {
|
|||||||
--imex-form-surface: rgba(255, 255, 255, 0.01); /* subtle lift off page bg */
|
--imex-form-surface: rgba(255, 255, 255, 0.01); /* subtle lift off page bg */
|
||||||
--imex-form-surface-head: rgba(255, 255, 255, 0.06); /* slightly stronger for header strip */
|
--imex-form-surface-head: rgba(255, 255, 255, 0.06); /* slightly stronger for header strip */
|
||||||
--imex-form-surface-border: rgba(5, 5, 5, 0.12);
|
--imex-form-surface-border: rgba(5, 5, 5, 0.12);
|
||||||
|
--imex-form-title-input-bg: rgba(255, 255, 255, 0.12);
|
||||||
|
--imex-form-title-input-border: rgba(255, 255, 255, 0.2);
|
||||||
|
--imex-form-title-group-bg: rgba(255, 255, 255, 0.08);
|
||||||
|
--imex-form-title-group-border: rgba(255, 255, 255, 0.16);
|
||||||
|
--imex-form-title-label-bg: rgba(255, 255, 255, 0.06);
|
||||||
|
--imex-form-title-label-border: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.imex-form-row {
|
.imex-form-row {
|
||||||
@@ -38,18 +50,111 @@ html[data-theme="dark"] {
|
|||||||
border-color: var(--imex-form-surface-border);
|
border-color: var(--imex-form-surface-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.imex-form-row--error.ant-card {
|
||||||
|
border-color: var(--ant-color-error);
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ant-color-error) 24%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card-head {
|
.ant-card-head {
|
||||||
background: var(--imex-form-surface-head);
|
background: var(--imex-form-surface-head);
|
||||||
border-bottom-color: var(--imex-form-surface-border);
|
border-bottom-color: var(--imex-form-surface-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.imex-form-row--error {
|
||||||
|
.ant-card-head,
|
||||||
|
.ant-card-actions {
|
||||||
|
border-color: color-mix(in srgb, var(--ant-color-error) 34%, var(--imex-form-surface-border));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.imex-form-row--compact {
|
||||||
|
.ant-card-head {
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title,
|
||||||
|
.ant-card-extra {
|
||||||
|
padding-block: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.imex-form-row--title-only {
|
||||||
|
.ant-card-head {
|
||||||
|
min-height: auto;
|
||||||
|
padding-inline: 6px;
|
||||||
|
padding-block: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-wrapper {
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title,
|
||||||
|
.ant-card-extra {
|
||||||
|
padding-block: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
white-space: normal;
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: unset;
|
||||||
|
font-size: var(--ant-font-size);
|
||||||
|
line-height: 1.1;
|
||||||
|
padding-inline: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
display: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input,
|
||||||
|
.ant-input-number,
|
||||||
|
.ant-input-affix-wrapper,
|
||||||
|
.ant-select-selector,
|
||||||
|
.ant-picker {
|
||||||
|
background: var(--imex-form-title-input-bg);
|
||||||
|
border-color: var(--imex-form-title-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number-input {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
background: var(--imex-form-surface);
|
background: var(--imex-form-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-actions {
|
||||||
|
background: var(--imex-form-surface-head);
|
||||||
|
border-top-color: var(--imex-form-surface-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-actions > li {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-actions .ant-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item:last-child {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Optional: tighter spacing on phones for better space usage */
|
/* Optional: tighter spacing on phones for better space usage */
|
||||||
@media (max-width: 575px) {
|
@media (max-width: 575px) {
|
||||||
.ant-card-head {
|
&:not(.imex-form-row--title-only) .ant-card-head {
|
||||||
padding-inline: 12px;
|
padding-inline: 12px;
|
||||||
padding-block: 12px;
|
padding-block: 12px;
|
||||||
}
|
}
|
||||||
@@ -70,6 +175,14 @@ html[data-theme="dark"] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-form-item:has(.imex-form-row--compact) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item:has(.imex-form-row--title-only) {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Better form item spacing on mobile */
|
/* Better form item spacing on mobile */
|
||||||
@media (max-width: 575px) {
|
@media (max-width: 575px) {
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
@@ -77,3 +190,24 @@ html[data-theme="dark"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.imex-form-row-empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ant-color-text-description);
|
||||||
|
font-size: var(--ant-font-size);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imex-inline-form-row-errors {
|
||||||
|
color: var(--ant-color-error);
|
||||||
|
|
||||||
|
.ant-form-item-explain,
|
||||||
|
.ant-form-item-explain-error,
|
||||||
|
.ant-form-item-additional {
|
||||||
|
color: var(--ant-color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||||
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 { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||||
@@ -50,6 +51,7 @@ export function PartsOrderModalComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const partsOrderLines = Form.useWatch(["parts_order_lines", "data"], form) || [];
|
||||||
const handleClick = ({ item }) => {
|
const handleClick = ({ item }) => {
|
||||||
form.setFieldsValue({ comments: item.props.value });
|
form.setFieldsValue({ comments: item.props.value });
|
||||||
};
|
};
|
||||||
@@ -128,10 +130,38 @@ export function PartsOrderModalComponent({
|
|||||||
{(fields, { remove, move }) => {
|
{(fields, { remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<Form.Item required={false} key={field.key}>
|
const partsOrderLine = partsOrderLines[field.name] || {};
|
||||||
<div style={{ display: "flex" }}>
|
|
||||||
<LayoutFormRow grow noDivider style={{ flex: 1 }}>
|
return (
|
||||||
|
<Form.Item required={false} key={field.key}>
|
||||||
|
<LayoutFormRow
|
||||||
|
grow
|
||||||
|
noDivider
|
||||||
|
title={getFormListItemTitle(
|
||||||
|
t("parts_orders.fields.line_desc"),
|
||||||
|
index,
|
||||||
|
partsOrderLine.line_desc,
|
||||||
|
partsOrderLine.oem_partno
|
||||||
|
)}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
//span={8}
|
//span={8}
|
||||||
label={t("parts_orders.fields.line_desc")}
|
label={t("parts_orders.fields.line_desc")}
|
||||||
@@ -146,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`}
|
||||||
@@ -220,20 +253,9 @@ export function PartsOrderModalComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Space wrap size="small" align="center">
|
</Form.Item>
|
||||||
<div>
|
);
|
||||||
<DeleteFilled
|
})}
|
||||||
style={{ margin: "1rem" }}
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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,10 +1,11 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { Form, Input, InputNumber, Select, Typography } from "antd";
|
import { Button, Form, Input, InputNumber, Select, Space, Typography } from "antd";
|
||||||
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 { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -15,6 +16,7 @@ export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
|
|||||||
|
|
||||||
export function PartsReceiveModalComponent({ bodyshop, form }) {
|
export function PartsReceiveModalComponent({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const partsOrderLines = Form.useWatch(["partsorderlines"], form) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -42,16 +44,43 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
|||||||
{(fields, { remove, move }) => {
|
{(fields, { remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<Form.Item required={false} key={field.key}>
|
const partsOrderLine = partsOrderLines[field.name] || {};
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
|
return (
|
||||||
|
<Form.Item required={false} key={field.key}>
|
||||||
<Form.Item hidden key={`${index}joblineid`} name={[field.name, "joblineid"]}>
|
<Form.Item hidden key={`${index}joblineid`} name={[field.name, "joblineid"]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item hidden key={`${index}id`} name={[field.name, "id"]}>
|
<Form.Item hidden key={`${index}id`} name={[field.name, "id"]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<LayoutFormRow grow style={{ flex: 1 }}>
|
<LayoutFormRow
|
||||||
|
grow
|
||||||
|
title={getFormListItemTitle(
|
||||||
|
t("parts_orders.fields.line_desc"),
|
||||||
|
index,
|
||||||
|
partsOrderLine.line_desc,
|
||||||
|
partsOrderLine.oem_partno
|
||||||
|
)}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("parts_orders.fields.line_desc")}
|
label={t("parts_orders.fields.line_desc")}
|
||||||
key={`${index}line_desc`}
|
key={`${index}line_desc`}
|
||||||
@@ -84,7 +113,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
|||||||
key={`${index}location`}
|
key={`${index}location`}
|
||||||
name={[field.name, "location"]}
|
name={[field.name, "location"]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: "10rem" }}
|
style={{ width: "10rem" }}
|
||||||
options={bodyshop.md_parts_locations.map((loc, idx) => ({
|
options={bodyshop.md_parts_locations.map((loc, idx) => ({
|
||||||
key: idx,
|
key: idx,
|
||||||
@@ -101,16 +130,9 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
|||||||
<InputNumber min={0} />
|
<InputNumber min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<DeleteFilled
|
</Form.Item>
|
||||||
style={{ margin: "1rem" }}
|
);
|
||||||
onClick={() => {
|
})}
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { DeleteFilled } from "@ant-design/icons";
|
|||||||
import { Button, Form, Input, Select, Space } from "antd";
|
import { Button, Form, Input, Select, Space } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
export default function PartsEmailPresetsComponent() {
|
export default function PartsEmailPresetsComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const emailPresets = Form.useWatch(["md_to_emails"], form) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -14,31 +17,46 @@ export default function PartsEmailPresetsComponent() {
|
|||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<Form.Item key={field.key}>
|
const preset = emailPresets[field.name] || {};
|
||||||
<LayoutFormRow noDivider>
|
|
||||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.labels.md_to_emails_emails")}
|
|
||||||
key={`${index}emails`}
|
|
||||||
name={[field.name, "emails"]}
|
|
||||||
>
|
|
||||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Space>
|
return (
|
||||||
<DeleteFilled
|
<Form.Item key={field.key}>
|
||||||
onClick={() => {
|
<LayoutFormRow
|
||||||
remove(field.name);
|
noDivider
|
||||||
}}
|
title={getFormListItemTitle(t("general.labels.label"), index, preset.label, preset.emails)}
|
||||||
/>
|
extra={
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
<Space align="center" size="small">
|
||||||
</Space>
|
<Button
|
||||||
</LayoutFormRow>
|
type="text"
|
||||||
</Form.Item>
|
icon={<DeleteFilled />}
|
||||||
))}
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.md_to_emails_emails")}
|
||||||
|
key={`${index}emails`}
|
||||||
|
name={[field.name, "emails"]}
|
||||||
|
>
|
||||||
|
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { DeleteFilled } from "@ant-design/icons";
|
|||||||
import { Button, Form, Input, Space } from "antd";
|
import { Button, Form, Input, Space } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
export default function PartsLocationsComponent() {
|
export default function PartsLocationsComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const partsLocations = Form.useWatch(["md_parts_locations"], form) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -14,34 +17,49 @@ export default function PartsLocationsComponent() {
|
|||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<Form.Item key={field.key}>
|
const location = partsLocations[field.name];
|
||||||
<LayoutFormRow noDivider>
|
|
||||||
<Form.Item
|
return (
|
||||||
className="imex-flex-row__margin"
|
<Form.Item key={field.key}>
|
||||||
label={t("bodyshop.fields.partslocation")}
|
<LayoutFormRow
|
||||||
key={`${index}`}
|
noDivider
|
||||||
name={[field.name]}
|
title={getFormListItemTitle(t("bodyshop.fields.partslocation"), index, location)}
|
||||||
rules={[
|
extra={
|
||||||
{
|
<Space align="center" size="small">
|
||||||
required: true
|
<Button
|
||||||
}
|
type="text"
|
||||||
]}
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input />
|
<Form.Item
|
||||||
</Form.Item>
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
className="imex-flex-row__margin"
|
className="imex-flex-row__margin"
|
||||||
onClick={() => {
|
label={t("bodyshop.fields.partslocation")}
|
||||||
remove(field.name);
|
key={`${index}`}
|
||||||
}}
|
name={[field.name]}
|
||||||
/>
|
rules={[
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
{
|
||||||
</Space>
|
required: true
|
||||||
</LayoutFormRow>
|
}
|
||||||
</Form.Item>
|
]}
|
||||||
))}
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { DeleteFilled } from "@ant-design/icons";
|
|||||||
import { Button, Form, Input, Space } from "antd";
|
import { Button, Form, Input, Space } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
export default function PartsOrderCommentsComponent() {
|
export default function PartsOrderCommentsComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const orderComments = Form.useWatch(["md_parts_order_comment"], form) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -14,45 +17,65 @@ export default function PartsOrderCommentsComponent() {
|
|||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => {
|
||||||
<Form.Item key={field.key}>
|
const comment = orderComments[field.name] || {};
|
||||||
<LayoutFormRow noDivider>
|
|
||||||
<Form.Item
|
|
||||||
label={t("general.labels.label")}
|
|
||||||
key={`${index}label`}
|
|
||||||
name={[field.name, "label"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("parts_orders.fields.comments")}
|
|
||||||
key={`${index}comment`}
|
|
||||||
name={[field.name, "comment"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Space wrap>
|
return (
|
||||||
<DeleteFilled
|
<Form.Item key={field.key}>
|
||||||
onClick={() => {
|
<LayoutFormRow
|
||||||
remove(field.name);
|
noDivider
|
||||||
}}
|
title={getFormListItemTitle(
|
||||||
/>
|
t("parts_orders.fields.comments"),
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
index,
|
||||||
</Space>
|
comment.label,
|
||||||
</LayoutFormRow>
|
comment.comment
|
||||||
</Form.Item>
|
)}
|
||||||
))}
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("general.labels.label")}
|
||||||
|
key={`${index}label`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("parts_orders.fields.comments")}
|
||||||
|
key={`${index}comment`}
|
||||||
|
name={[field.name, "comment"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.compon
|
|||||||
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";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -36,6 +36,8 @@ 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 Templates = !hasDMSKey
|
const Templates = !hasDMSKey
|
||||||
? Object.keys(tempList)
|
? Object.keys(tempList)
|
||||||
@@ -60,6 +62,7 @@ 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 =
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function ProductionColumnsComponent({
|
|||||||
|
|
||||||
const columnKeys = columns.map((i) => i.key);
|
const columnKeys = columns.map((i) => i.key);
|
||||||
const cols = dataSource({
|
const cols = dataSource({
|
||||||
|
bodyshop,
|
||||||
technician,
|
technician,
|
||||||
data,
|
data,
|
||||||
state: tableState,
|
state: tableState,
|
||||||
|
|||||||
@@ -609,7 +609,19 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
|
||||||
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
|
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
|
||||||
}
|
},
|
||||||
|
...(bodyshop && bodyshop.rr_dealerid
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: i18n.t("jobs.fields.dms.id"),
|
||||||
|
dataIndex: "dms_id",
|
||||||
|
key: "dms_id",
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: (a, b) => alphaSort(a.dms_id, b.dms_id),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
export default productionListColumnsData;
|
export default productionListColumnsData;
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ export function ProductionListConfigManager({
|
|||||||
nextConfig.columns.columnKeys.map((k) => {
|
nextConfig.columns.columnKeys.map((k) => {
|
||||||
return {
|
return {
|
||||||
...ProductionListColumns({
|
...ProductionListColumns({
|
||||||
|
bodyshop,
|
||||||
technician,
|
technician,
|
||||||
state: ensureDefaultState(state),
|
state: ensureDefaultState(state),
|
||||||
refetch,
|
refetch,
|
||||||
@@ -270,6 +271,7 @@ export function ProductionListConfigManager({
|
|||||||
activeConfig.columns.columnKeys.map((k) => {
|
activeConfig.columns.columnKeys.map((k) => {
|
||||||
return {
|
return {
|
||||||
...ProductionListColumns({
|
...ProductionListColumns({
|
||||||
|
bodyshop,
|
||||||
technician,
|
technician,
|
||||||
state: ensureDefaultState(state),
|
state: ensureDefaultState(state),
|
||||||
refetch,
|
refetch,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { INSERT_VACATION } from "../../graphql/employees.queries";
|
|||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
export default function ShopEmployeeAddVacation({ employee }) {
|
export default function ShopEmployeeAddVacation({ employee, buttonProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertVacation] = useMutation(INSERT_VACATION);
|
const [insertVacation] = useMutation(INSERT_VACATION);
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={overlay} open={visibility}>
|
<Popover content={overlay} open={visibility}>
|
||||||
<Button loading={loading} disabled={!employee?.active} onClick={handleClick}>
|
<Button loading={loading} disabled={!employee?.active} onClick={handleClick} {...buttonProps}>
|
||||||
{t("employees.actions.addvacation")}
|
{t("employees.actions.addvacation")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Form, Input, InputNumber, Select, Switch } from "antd";
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useForm } from "antd/es/form/Form";
|
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useEffect } 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 { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -26,9 +25,24 @@ import { DateFormatter } from "../../utils/DateFormatter";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_TEXT_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
|
||||||
|
import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -37,19 +51,38 @@ const mapDispatchToProps = () => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ShopEmployeesFormComponent({ bodyshop }) {
|
export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
||||||
|
const submitActionRef = useRef("save");
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = useForm();
|
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
||||||
|
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
||||||
|
const employeeNumber = Form.useWatch("employee_number", form);
|
||||||
|
const firstName = Form.useWatch("first_name", form);
|
||||||
|
const lastName = Form.useWatch("last_name", form);
|
||||||
|
const employeeOptionsColProps = {
|
||||||
|
xs: 24,
|
||||||
|
sm: 12,
|
||||||
|
md: 12,
|
||||||
|
lg: 8,
|
||||||
|
xl: 8,
|
||||||
|
xxl: 8
|
||||||
|
};
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const [deleteVacation] = useMutation(DELETE_VACATION);
|
const [deleteVacation] = useMutation(DELETE_VACATION);
|
||||||
const { error, data } = useQuery(QUERY_EMPLOYEE_BY_ID, {
|
const { error, data, refetch } = useQuery(QUERY_EMPLOYEE_BY_ID, {
|
||||||
variables: { id: search.employeeId },
|
variables: { id: search.employeeId },
|
||||||
skip: !search.employeeId || search.employeeId === "new",
|
skip: !search.employeeId || search.employeeId === "new",
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const isNewEmployee = search.employeeId === "new";
|
||||||
|
const currentEmployeeData = data?.employees_by_pk?.id === search.employeeId ? data.employees_by_pk : null;
|
||||||
|
const employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||||
|
const employeeCardTitle =
|
||||||
|
[employeeNumber, employeeTitleName].filter(Boolean).join(" - ") ||
|
||||||
|
(isNewEmployee ? t("employees.actions.new") : t("bodyshop.labels.employees"));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Enhanced_Payroll }
|
treatments: { Enhanced_Payroll }
|
||||||
@@ -59,56 +92,154 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateDirtyState = useCallback(
|
||||||
|
(nextDirtyState) => {
|
||||||
|
setInternalIsDirty(nextDirtyState);
|
||||||
|
onDirtyChange?.(nextDirtyState);
|
||||||
|
},
|
||||||
|
[onDirtyChange]
|
||||||
|
);
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
useEffect(() => {
|
const clearEmployeeFormMeta = useCallback(() => {
|
||||||
if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk);
|
const fieldMeta = form.getFieldsError().map(({ name }) => ({
|
||||||
else {
|
name,
|
||||||
form.resetFields();
|
touched: false,
|
||||||
|
validating: false,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (fieldMeta.length > 0) {
|
||||||
|
form.setFields(fieldMeta);
|
||||||
}
|
}
|
||||||
}, [form, data, search.employeeId]);
|
|
||||||
|
updateDirtyState(false);
|
||||||
|
}, [form, updateDirtyState]);
|
||||||
|
|
||||||
|
const resetEmployeeFormToCurrentData = useCallback(() => {
|
||||||
|
form.resetFields();
|
||||||
|
|
||||||
|
if (currentEmployeeData) {
|
||||||
|
form.setFieldsValue(currentEmployeeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
clearEmployeeFormMeta();
|
||||||
|
});
|
||||||
|
}, [clearEmployeeFormMeta, currentEmployeeData, form]);
|
||||||
|
|
||||||
|
const syncEmployeeFormToSavedData = useCallback(
|
||||||
|
(employeeData) => {
|
||||||
|
if (employeeData) {
|
||||||
|
form.setFieldsValue(employeeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
clearEmployeeFormMeta();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[clearEmployeeFormMeta, form]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetEmployeeFormToCurrentData();
|
||||||
|
}, [resetEmployeeFormToCurrentData, search.employeeId]);
|
||||||
|
|
||||||
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
const [updateEmployee] = useMutation(UPDATE_EMPLOYEE);
|
||||||
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
const [insertEmployees] = useMutation(INSERT_EMPLOYEES);
|
||||||
|
const saveAndResetSubmitAction = useCallback(() => {
|
||||||
|
const submitAction = submitActionRef.current;
|
||||||
|
submitActionRef.current = "save";
|
||||||
|
return submitAction;
|
||||||
|
}, []);
|
||||||
|
const submitEmployeeForm = useCallback(
|
||||||
|
(submitAction = "save") => {
|
||||||
|
submitActionRef.current = submitAction;
|
||||||
|
form.submit();
|
||||||
|
},
|
||||||
|
[form]
|
||||||
|
);
|
||||||
|
const navigateToEmployee = useCallback(
|
||||||
|
(employeeId) => {
|
||||||
|
history({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[history, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFinish = async (values) => {
|
||||||
|
const submitAction = saveAndResetSubmitAction();
|
||||||
|
const normalizedValues = {
|
||||||
|
...values,
|
||||||
|
user_email: values.user_email === "" ? null : values.user_email
|
||||||
|
};
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
|
||||||
if (search.employeeId && search.employeeId !== "new") {
|
if (search.employeeId && search.employeeId !== "new") {
|
||||||
//Update a record.
|
//Update a record.
|
||||||
logImEXEvent("shop_employee_update");
|
logImEXEvent("shop_employee_update");
|
||||||
|
|
||||||
updateEmployee({
|
try {
|
||||||
variables: {
|
const result = await updateEmployee({
|
||||||
id: search.employeeId,
|
variables: {
|
||||||
employee: {
|
id: search.employeeId,
|
||||||
...values,
|
employee: normalizedValues
|
||||||
user_email: values.user_email === "" ? null : values.user_email
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
notification.success({
|
|
||||||
title: t("employees.successes.save")
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
notification.error({
|
|
||||||
title: t("employees.errors.save", {
|
|
||||||
message: JSON.stringify(error)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
//New record, insert it.
|
|
||||||
logImEXEvent("shop_employee_insert");
|
|
||||||
|
|
||||||
insertEmployees({
|
syncEmployeeFormToSavedData(result?.data?.update_employees?.returning?.[0] ?? normalizedValues);
|
||||||
variables: { employees: [{ ...values, shopid: bodyshop.id }] },
|
void refetch();
|
||||||
refetchQueries: ["QUERY_EMPLOYEES"]
|
if (submitAction === "saveAndNew") {
|
||||||
}).then((r) => {
|
navigateToEmployee("new");
|
||||||
search.employeeId = r.data.insert_employees.returning[0].id;
|
}
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employees.errors.save", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//New record, insert it.
|
||||||
|
logImEXEvent("shop_employee_insert");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await insertEmployees({
|
||||||
|
variables: { employees: [{ ...normalizedValues, shopid: bodyshop.id }] },
|
||||||
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
||||||
|
});
|
||||||
|
const savedEmployee = result?.data?.insert_employees?.returning?.[0];
|
||||||
|
|
||||||
|
if (submitAction === "saveAndNew") {
|
||||||
|
if (isNewEmployee) {
|
||||||
|
resetEmployeeFormToCurrentData();
|
||||||
|
}
|
||||||
|
navigateToEmployee("new");
|
||||||
|
} else if (savedEmployee?.id) {
|
||||||
|
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
||||||
|
navigateToEmployee(savedEmployee.id);
|
||||||
|
} else {
|
||||||
|
syncEmployeeFormToSavedData(savedEmployee ?? normalizedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.success({
|
||||||
|
title: t("employees.successes.save")
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
title: t("employees.errors.save", {
|
||||||
|
message: JSON.stringify(error)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -141,6 +272,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Button
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteVacation({
|
await deleteVacation({
|
||||||
variables: { id: record.id },
|
variables: { id: record.id },
|
||||||
@@ -168,225 +301,365 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
title={employeeCardTitle}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => form.submit()}>
|
<Space wrap>
|
||||||
{t("general.actions.save")}
|
<Button onClick={() => submitEmployeeForm("saveAndNew")} disabled={!resolvedIsDirty} style={{ minWidth: 170 }}>
|
||||||
</Button>
|
{t("general.actions.saveandnew") || "Save and New"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => submitEmployeeForm("save")}
|
||||||
|
disabled={!resolvedIsDirty}
|
||||||
|
style={{ minWidth: 170 }}
|
||||||
|
>
|
||||||
|
{t("employees.actions.save_employee")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form
|
||||||
<LayoutFormRow>
|
onFinish={handleFinish}
|
||||||
<Form.Item
|
onFinishFailed={saveAndResetSubmitAction}
|
||||||
name="first_name"
|
autoComplete={"off"}
|
||||||
label={t("employees.fields.first_name")}
|
layout="vertical"
|
||||||
rules={[
|
form={form}
|
||||||
{
|
onValuesChange={() => {
|
||||||
required: true
|
updateDirtyState(form.isFieldsTouched());
|
||||||
//message: t("general.validation.required"),
|
}}
|
||||||
}
|
>
|
||||||
]}
|
<FormsFieldChanged form={form} onReset={resetEmployeeFormToCurrentData} onDirtyChange={updateDirtyState} />
|
||||||
>
|
<LayoutFormRow
|
||||||
<Input />
|
title={
|
||||||
</Form.Item>
|
<div
|
||||||
<Form.Item
|
style={{
|
||||||
label={t("employees.fields.last_name")}
|
...INLINE_TITLE_ROW_STYLE,
|
||||||
name="last_name"
|
justifyContent: "space-between"
|
||||||
rules={[
|
}}
|
||||||
{
|
>
|
||||||
required: true
|
<div
|
||||||
//message: t("general.validation.required"),
|
style={{
|
||||||
}
|
...INLINE_TITLE_TEXT_STYLE,
|
||||||
]}
|
marginRight: "auto"
|
||||||
>
|
}}
|
||||||
<Input />
|
>
|
||||||
</Form.Item>
|
{t("bodyshop.labels.employee_options")}
|
||||||
<Form.Item
|
</div>
|
||||||
name="employee_number"
|
<div
|
||||||
label={t("employees.fields.employee_number")}
|
style={{
|
||||||
validateTrigger="onBlur"
|
display: "flex",
|
||||||
hasFeedback
|
alignItems: "center",
|
||||||
rules={[
|
gap: 4,
|
||||||
{
|
flexWrap: "wrap",
|
||||||
required: true
|
marginLeft: "auto"
|
||||||
//message: t("general.validation.required"),
|
}}
|
||||||
},
|
>
|
||||||
() => ({
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
async validator(rule, value) {
|
<div
|
||||||
if (value) {
|
style={{
|
||||||
const response = await client.query({
|
...INLINE_TITLE_SWITCH_GROUP_STYLE
|
||||||
query: CHECK_EMPLOYEE_NUMBER,
|
}}
|
||||||
variables: {
|
>
|
||||||
employeenumber: value
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.labels.active")}</div>
|
||||||
}
|
<Form.Item noStyle valuePropName="checked" name="active">
|
||||||
});
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
if (response.data.employees_aggregate.aggregate.count === 0) {
|
</div>
|
||||||
return Promise.resolve();
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
} else if (
|
<div
|
||||||
response.data.employees_aggregate.nodes.length === 1 &&
|
style={{
|
||||||
response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id")
|
...INLINE_TITLE_SWITCH_GROUP_STYLE
|
||||||
) {
|
}}
|
||||||
return Promise.resolve();
|
>
|
||||||
}
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.flat_rate")}</div>
|
||||||
return Promise.reject(t("employees.validation.unique_employee_number"));
|
<Form.Item noStyle valuePropName="checked" name="flat_rate">
|
||||||
} else {
|
<Switch />
|
||||||
return Promise.resolve();
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
>
|
||||||
|
<Row gutter={[16, 16]} wrap>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item
|
||||||
|
name="first_name"
|
||||||
|
label={t("employees.fields.first_name")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
}
|
]}
|
||||||
})
|
>
|
||||||
]}
|
<Input />
|
||||||
>
|
</Form.Item>
|
||||||
<Input />
|
</Col>
|
||||||
</Form.Item>
|
<Col {...employeeOptionsColProps}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("employees.fields.pin")}
|
label={t("employees.fields.last_name")}
|
||||||
name="pin"
|
name="last_name"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
|
||||||
<LayoutFormRow>
|
|
||||||
<Form.Item label={t("employees.fields.active")} valuePropName="checked" name="active">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t("employees.fields.flat_rate")} name="flat_rate" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="hire_date"
|
|
||||||
label={t("employees.fields.hire_date")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<DateTimePicker isDateOnly />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
|
|
||||||
<DateTimePicker isDateOnly />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("employees.fields.user_email")}
|
|
||||||
name="user_email"
|
|
||||||
validateTrigger="onBlur"
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
async validator(rule, value) {
|
|
||||||
const user_email = getFieldValue("user_email");
|
|
||||||
|
|
||||||
if (user_email && value) {
|
|
||||||
const response = await client.query({
|
|
||||||
query: QUERY_USERS_BY_EMAIL,
|
|
||||||
variables: {
|
|
||||||
email: user_email
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.users.length === 1) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
}
|
]}
|
||||||
})
|
>
|
||||||
]}
|
<Input />
|
||||||
>
|
</Form.Item>
|
||||||
<Input />
|
</Col>
|
||||||
</Form.Item>
|
<Col {...employeeOptionsColProps}>
|
||||||
<Form.Item label={t("employees.fields.external_id")} name="external_id">
|
<Form.Item
|
||||||
<Input />
|
name="employee_number"
|
||||||
</Form.Item>
|
label={t("employees.fields.employee_number")}
|
||||||
|
validateTrigger="onBlur"
|
||||||
|
hasFeedback
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
() => ({
|
||||||
|
async validator(rule, value) {
|
||||||
|
if (value) {
|
||||||
|
const response = await client.query({
|
||||||
|
query: CHECK_EMPLOYEE_NUMBER,
|
||||||
|
variables: {
|
||||||
|
employeenumber: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.employees_aggregate.aggregate.count === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if (
|
||||||
|
response.data.employees_aggregate.nodes.length === 1 &&
|
||||||
|
response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id")
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(t("employees.validation.unique_employee_number"));
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("employees.fields.pin")}
|
||||||
|
name="pin"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item
|
||||||
|
name="hire_date"
|
||||||
|
label={t("employees.fields.hire_date")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DateTimePicker isDateOnly />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
|
||||||
|
<DateTimePicker isDateOnly />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("employees.fields.user_email")}
|
||||||
|
name="user_email"
|
||||||
|
validateTrigger="onBlur"
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
async validator(rule, value) {
|
||||||
|
const user_email = getFieldValue("user_email");
|
||||||
|
|
||||||
|
if (user_email && value) {
|
||||||
|
const response = await client.query({
|
||||||
|
query: QUERY_USERS_BY_EMAIL,
|
||||||
|
variables: {
|
||||||
|
email: user_email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.users.length === 1) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormItemEmail />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col {...employeeOptionsColProps}>
|
||||||
|
<Form.Item label={t("employees.fields.external_id")} name="external_id">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Form.List name={["rates"]}>
|
<Form.List name={["rates"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
{fields.map((field, index) => (
|
title={t("bodyshop.labels.employee_rates")}
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
actions={[
|
||||||
<LayoutFormRow grow>
|
|
||||||
<Form.Item
|
|
||||||
label={t("employees.fields.cost_center")}
|
|
||||||
key={`${field.key}-cost_center`}
|
|
||||||
name={[field.name, "cost_center"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
|
|
||||||
...(bodyshop.cdk_dealerid ||
|
|
||||||
bodyshop.pbs_serialnumber ||
|
|
||||||
bodyshop.rr_dealerid ||
|
|
||||||
Enhanced_Payroll.treatment === "on"
|
|
||||||
? CiecaSelect(false, true)
|
|
||||||
: bodyshop.md_responsibility_centers.costs.map((c) => ({
|
|
||||||
value: c.name,
|
|
||||||
label: c.name
|
|
||||||
})))
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("employees.fields.rate")}
|
|
||||||
key={`${field.key}-rate`}
|
|
||||||
name={[field.name, "rate"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
key="add-rate"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add();
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
|
||||||
id="add-employee-rate-button"
|
id="add-employee-rate-button"
|
||||||
>
|
>
|
||||||
<span id="new-employee-rate">{t("employees.actions.newrate")}</span>
|
<span id="new-employee-rate">{t("employees.actions.addrate")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
]}
|
||||||
</div>
|
>
|
||||||
|
<div>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<ConfigListEmptyState actionLabel={t("employees.actions.addrate")} />
|
||||||
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle key={field.key}>
|
||||||
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["rates", field.name, "cost_center"]]}
|
||||||
|
noDivider
|
||||||
|
title={
|
||||||
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employees.fields.cost_center")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "cost_center"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
options={[
|
||||||
|
{ value: "timetickets.labels.shift", label: t("timetickets.labels.shift") },
|
||||||
|
...(bodyshop.cdk_dealerid ||
|
||||||
|
bodyshop.pbs_serialnumber ||
|
||||||
|
bodyshop.rr_dealerid ||
|
||||||
|
Enhanced_Payroll.treatment === "on"
|
||||||
|
? CiecaSelect(false, true)
|
||||||
|
: bodyshop.md_responsibility_centers.costs.map((c) => ({
|
||||||
|
value: c.name,
|
||||||
|
label: c.name
|
||||||
|
})))
|
||||||
|
]}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
styles={{
|
||||||
|
selector: INLINE_TITLE_INPUT_STYLE
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("employees.fields.rate")}
|
||||||
|
name={[field.name, "rate"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} precision={2} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<ResponsiveTable
|
<LayoutFormRow
|
||||||
title={() => <ShopEmployeeAddVacation employee={data && data.employees_by_pk} />}
|
title={t("bodyshop.labels.employee_vacation")}
|
||||||
columns={columns}
|
actions={[
|
||||||
mobileColumnKeys={["start", "length", "actions"]}
|
<ShopEmployeeAddVacation
|
||||||
rowKey={"id"}
|
key="add-vacation"
|
||||||
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
employee={data && data.employees_by_pk}
|
||||||
/>
|
buttonProps={{
|
||||||
|
type: "primary",
|
||||||
|
block: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{(data?.employees_by_pk?.employee_vacations ?? []).length === 0 ? (
|
||||||
|
<ConfigListEmptyState actionLabel={t("employees.actions.addvacation")} />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<ResponsiveTable
|
||||||
|
columns={columns}
|
||||||
|
mobileColumnKeys={["start", "length", "actions"]}
|
||||||
|
rowKey={"id"}
|
||||||
|
dataSource={data?.employees_by_pk?.employee_vacations ?? []}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</LayoutFormRow>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
|
import { Form } from "antd";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
DELETE_VACATION,
|
||||||
|
INSERT_EMPLOYEES,
|
||||||
|
QUERY_EMPLOYEE_BY_ID,
|
||||||
|
UPDATE_EMPLOYEE
|
||||||
|
} from "../../graphql/employees.queries";
|
||||||
|
import { ShopEmployeesFormComponent } from "./shop-employees-form.component.jsx";
|
||||||
|
|
||||||
|
const insertEmployeesMock = vi.fn();
|
||||||
|
const updateEmployeeMock = vi.fn();
|
||||||
|
const deleteVacationMock = vi.fn();
|
||||||
|
const useQueryMock = vi.fn();
|
||||||
|
const useMutationMock = vi.fn();
|
||||||
|
const navigateMock = vi.fn();
|
||||||
|
const notification = {
|
||||||
|
error: vi.fn(),
|
||||||
|
success: vi.fn()
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("@apollo/client/react", async () => {
|
||||||
|
const actual = await vi.importActual("@apollo/client/react");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useApolloClient: vi.fn(),
|
||||||
|
useQuery: (...args) => useQueryMock(...args),
|
||||||
|
useMutation: (...args) => useMutationMock(...args)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@splitsoftware/splitio-react", () => ({
|
||||||
|
useTreatmentsWithConfig: () => ({
|
||||||
|
treatments: {
|
||||||
|
Enhanced_Payroll: {
|
||||||
|
treatment: "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-router-dom", () => ({
|
||||||
|
useLocation: () => ({
|
||||||
|
search: "?employeeId=new"
|
||||||
|
}),
|
||||||
|
useNavigate: () => navigateMock
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key, values = {}) => {
|
||||||
|
const translations = {
|
||||||
|
"bodyshop.labels.employee_options": "Employee Options",
|
||||||
|
"bodyshop.labels.employee_rates": "Employee Rates",
|
||||||
|
"bodyshop.labels.employee_vacation": "Employee Vacation",
|
||||||
|
"bodyshop.labels.employees": "Employees",
|
||||||
|
"employees.actions.addrate": "Add Rate",
|
||||||
|
"employees.actions.addvacation": "Add Vacation",
|
||||||
|
"employees.actions.new": "New Employee",
|
||||||
|
"employees.actions.save_employee": "Save Employee",
|
||||||
|
"employees.fields.active": "Active",
|
||||||
|
"employees.fields.employee_number": "Employee Number",
|
||||||
|
"employees.fields.external_id": "External Id",
|
||||||
|
"employees.fields.first_name": "First Name",
|
||||||
|
"employees.fields.flat_rate": "Flat Rate",
|
||||||
|
"employees.fields.hire_date": "Hire Date",
|
||||||
|
"employees.fields.last_name": "Last Name",
|
||||||
|
"employees.fields.pin": "PIN",
|
||||||
|
"employees.fields.rate": "Rate",
|
||||||
|
"employees.fields.termination_date": "Termination Date",
|
||||||
|
"employees.fields.user_email": "User Email",
|
||||||
|
"employees.labels.active": "Active",
|
||||||
|
"employees.successes.save": "Saved",
|
||||||
|
"general.actions.saveandnew": "Save and New",
|
||||||
|
"general.labels.actions": "Actions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key === "employees.errors.save") {
|
||||||
|
return `Save failed: ${values.message ?? ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "employees.validation.unique_employee_number") {
|
||||||
|
return "Employee number must be unique";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "bodyshop.validation.useremailmustexist") {
|
||||||
|
return "User email must exist";
|
||||||
|
}
|
||||||
|
|
||||||
|
return translations[key] || key;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
||||||
|
useNotification: () => notification
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../firebase/firebase.utils", () => ({
|
||||||
|
logImEXEvent: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../alert/alert.component", () => ({
|
||||||
|
default: ({ title }) => <div>{title}</div>
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-fields-changed-alert/form-fields-changed-alert.component.jsx", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-date-time-picker/form-date-time-picker.component.jsx", () => ({
|
||||||
|
default: ({ id, value, onChange }) => (
|
||||||
|
<input id={id} type="text" value={value ?? ""} onChange={(event) => onChange?.(event.target.value)} />
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-items-formatted/email-form-item.component.jsx", () => ({
|
||||||
|
default: ({ id, value, onChange }) => (
|
||||||
|
<input id={id} type="email" value={value ?? ""} onChange={(event) => onChange?.(event.target.value)} />
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
||||||
|
default: ({ title, extra, actions, children }) => (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{extra}
|
||||||
|
{children}
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/inline-validated-form-row.component.jsx", () => ({
|
||||||
|
default: ({ title, extra, children }) => (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{extra}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../layout-form-row/config-list-empty-state.component.jsx", () => ({
|
||||||
|
default: ({ actionLabel }) => <div>{actionLabel}</div>
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-list-move-arrows/form-list-move-arrows.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../responsive-table/responsive-table.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./shop-employees-add-vacation.component", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../utils/Ciecaselect", () => ({
|
||||||
|
default: () => []
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bodyshop = {
|
||||||
|
id: "shop-1",
|
||||||
|
imexshopid: "split-shop-1",
|
||||||
|
md_responsibility_centers: {
|
||||||
|
costs: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ShopEmployeesFormComponent", () => {
|
||||||
|
let formInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
useQueryMock.mockImplementation((query) => {
|
||||||
|
if (query === QUERY_EMPLOYEE_BY_ID) {
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
data: null,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
data: null,
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useMutationMock.mockImplementation((mutation) => {
|
||||||
|
if (mutation === INSERT_EMPLOYEES) return [insertEmployeesMock];
|
||||||
|
if (mutation === UPDATE_EMPLOYEE) return [updateEmployeeMock];
|
||||||
|
if (mutation === DELETE_VACATION) return [deleteVacationMock];
|
||||||
|
return [vi.fn()];
|
||||||
|
});
|
||||||
|
|
||||||
|
useApolloClient.mockReturnValue({
|
||||||
|
query: vi.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
employees_aggregate: {
|
||||||
|
aggregate: {
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
nodes: []
|
||||||
|
},
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
insertEmployeesMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
insert_employees: {
|
||||||
|
returning: [
|
||||||
|
{
|
||||||
|
id: "employee-123",
|
||||||
|
first_name: "Jamie",
|
||||||
|
last_name: "Rivera",
|
||||||
|
employee_number: "42",
|
||||||
|
active: true,
|
||||||
|
termination_date: null,
|
||||||
|
hire_date: "2026-04-20",
|
||||||
|
flat_rate: false,
|
||||||
|
rates: [],
|
||||||
|
pin: "1234",
|
||||||
|
user_email: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function TestHarness({ onFormReady }) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFormReady(form);
|
||||||
|
}, [form, onFormReady]);
|
||||||
|
|
||||||
|
return <ShopEmployeesFormComponent bodyshop={bodyshop} form={form} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestHarness
|
||||||
|
onFormReady={(form) => {
|
||||||
|
formInstance = form;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks a new employee form clean after save", async () => {
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
|
||||||
|
target: { value: "Jamie" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
|
||||||
|
target: { value: "Rivera" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
|
||||||
|
target: { value: "42" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
|
||||||
|
target: { value: "1234" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
|
||||||
|
target: { value: "2026-04-20" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveButton = screen.getByRole("button", { name: "Save Employee" });
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(saveButton.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(insertEmployeesMock).toHaveBeenCalledWith({
|
||||||
|
variables: {
|
||||||
|
employees: [
|
||||||
|
expect.objectContaining({
|
||||||
|
first_name: "Jamie",
|
||||||
|
last_name: "Rivera",
|
||||||
|
employee_number: "42",
|
||||||
|
pin: "1234",
|
||||||
|
hire_date: "2026-04-20",
|
||||||
|
shopid: "shop-1"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
refetchQueries: ["QUERY_EMPLOYEES"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(formInstance.isFieldsTouched()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notification.success).toHaveBeenCalledWith({
|
||||||
|
title: "Saved"
|
||||||
|
});
|
||||||
|
expect(navigateMock).toHaveBeenCalledWith({
|
||||||
|
search: "employeeId=employee-123"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves a new employee and opens a fresh employee form when save and new is clicked", async () => {
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
|
||||||
|
target: { value: "Jamie" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
|
||||||
|
target: { value: "Rivera" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
|
||||||
|
target: { value: "42" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
|
||||||
|
target: { value: "1234" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
|
||||||
|
target: { value: "2026-04-20" }
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save and New" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(insertEmployeesMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
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({
|
||||||
|
search: "employeeId=new"
|
||||||
|
});
|
||||||
|
expect(notification.success).toHaveBeenCalledWith({
|
||||||
|
title: "Saved"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,9 +4,16 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
|
|
||||||
export default function ShopEmployeesListComponent({ loading, employees }) {
|
export default function ShopEmployeesListComponent({
|
||||||
|
loading,
|
||||||
|
employees,
|
||||||
|
onRequestEmployeeChange,
|
||||||
|
selectedEmployeeId
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
@@ -16,13 +23,33 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
|||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const navigateToEmployee = (employeeId) => {
|
||||||
|
if (onRequestEmployeeChange) {
|
||||||
|
onRequestEmployeeChange(employeeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearEmployeeSelection = () => {
|
||||||
|
const { employeeId, ...nextSearch } = search;
|
||||||
|
void employeeId;
|
||||||
|
history({
|
||||||
|
search: queryString.stringify(nextSearch)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
const handleOnRowClick = (record) => {
|
||||||
if (record) {
|
if (record) {
|
||||||
search.employeeId = record.id;
|
navigateToEmployee(record.id);
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
} else {
|
} else {
|
||||||
delete search.employeeId;
|
clearEmployeeSelection();
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
@@ -30,7 +57,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
|||||||
};
|
};
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("employees.fields.employee_number"),
|
title: t("employees.labels.employee_number_short"),
|
||||||
dataIndex: "employee_number",
|
dataIndex: "employee_number",
|
||||||
key: "employee_number",
|
key: "employee_number",
|
||||||
sorter: (a, b) => alphaSort(a.employee_number, b.employee_number),
|
sorter: (a, b) => alphaSort(a.employee_number, b.employee_number),
|
||||||
@@ -89,44 +116,39 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
<ResponsiveTable
|
title={t("bodyshop.labels.employees")}
|
||||||
title={() => {
|
actions={[
|
||||||
return (
|
<Button key="new-employee" type="primary" block onClick={() => navigateToEmployee("new")}>
|
||||||
<Button
|
{t("employees.actions.new")}
|
||||||
type="primary"
|
</Button>
|
||||||
onClick={() => {
|
]}
|
||||||
search.employeeId = "new";
|
>
|
||||||
history({ search: queryString.stringify(search) });
|
{employees.length === 0 ? (
|
||||||
}}
|
<ConfigListEmptyState actionLabel={t("employees.actions.new")} />
|
||||||
>
|
) : (
|
||||||
{t("employees.actions.new")}
|
<ResponsiveTable
|
||||||
</Button>
|
loading={loading}
|
||||||
);
|
pagination={{ placement: "top" }}
|
||||||
}}
|
columns={columns}
|
||||||
loading={loading}
|
mobileColumnKeys={["employee_number", "employee_name", "active"]}
|
||||||
pagination={{ placement: "top" }}
|
rowKey="id"
|
||||||
columns={columns}
|
dataSource={employees}
|
||||||
mobileColumnKeys={["employee_number", "employee_name", "active"]}
|
rowSelection={{
|
||||||
rowKey="id"
|
onSelect: (props) => navigateToEmployee(props.id),
|
||||||
dataSource={employees}
|
type: "radio",
|
||||||
rowSelection={{
|
selectedRowKeys: [selectedEmployeeId || search.employeeId]
|
||||||
onSelect: (props) => {
|
}}
|
||||||
search.employeeId = props.id;
|
onChange={handleTableChange}
|
||||||
history({ search: queryString.stringify(search) });
|
onRow={(record) => {
|
||||||
},
|
return {
|
||||||
type: "radio",
|
onClick: () => {
|
||||||
selectedRowKeys: [search.employeeId]
|
handleOnRowClick(record);
|
||||||
}}
|
}
|
||||||
onChange={handleTableChange}
|
};
|
||||||
onRow={(record) => {
|
}}
|
||||||
return {
|
/>
|
||||||
onClick: () => {
|
)}
|
||||||
handleOnRowClick(record);
|
</LayoutFormRow>
|
||||||
}
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,101 @@
|
|||||||
|
import { Drawer, Form, Grid } from "antd";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
|
import queryString from "query-string";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
|
import { QUERY_EMPLOYEES } from "../../graphql/employees.queries";
|
||||||
|
import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ShopEmployeesFormComponent from "./shop-employees-form.component";
|
import ShopEmployeesFormComponent from "./shop-employees-form.component";
|
||||||
import ShopEmployeesListComponent from "./shop-employees-list.component";
|
import ShopEmployeesListComponent from "./shop-employees-list.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
import "./shop-employees.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
function ShopEmployeesContainer() {
|
function ShopEmployeesContainer() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isEmployeeFormDirty, setIsEmployeeFormDirty] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const search = queryString.parse(location.search);
|
||||||
const { loading, error, data } = useQuery(QUERY_EMPLOYEES, {
|
const { loading, error, data } = useQuery(QUERY_EMPLOYEES, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const screens = Grid.useBreakpoint();
|
||||||
|
const hasSelectedEmployee = Boolean(search.employeeId);
|
||||||
|
|
||||||
|
const bpoints = {
|
||||||
|
xs: "100%",
|
||||||
|
sm: "100%",
|
||||||
|
md: "92%",
|
||||||
|
lg: "80%",
|
||||||
|
xl: "80%",
|
||||||
|
xxl: "80%"
|
||||||
|
};
|
||||||
|
|
||||||
|
let drawerPercentage = "100%";
|
||||||
|
if (screens.xxl) drawerPercentage = bpoints.xxl;
|
||||||
|
else if (screens.xl) drawerPercentage = bpoints.xl;
|
||||||
|
else if (screens.lg) drawerPercentage = bpoints.lg;
|
||||||
|
else if (screens.md) drawerPercentage = bpoints.md;
|
||||||
|
else if (screens.sm) drawerPercentage = bpoints.sm;
|
||||||
|
else if (screens.xs) drawerPercentage = bpoints.xs;
|
||||||
|
|
||||||
|
const hasDirtyEmployeeForm = Boolean(search.employeeId) && (isEmployeeFormDirty || form.isFieldsTouched());
|
||||||
|
const confirmCloseDirtyEmployee = useConfirmDirtyFormNavigation(hasDirtyEmployeeForm);
|
||||||
|
|
||||||
|
const navigateToEmployee = (employeeId) => {
|
||||||
|
if (employeeId === search.employeeId) return;
|
||||||
|
if (!confirmCloseDirtyEmployee()) return;
|
||||||
|
|
||||||
|
const nextSearch = { ...search, employeeId };
|
||||||
|
setIsEmployeeFormDirty(false);
|
||||||
|
navigate({
|
||||||
|
search: queryString.stringify(nextSearch)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
if (!confirmCloseDirtyEmployee()) return;
|
||||||
|
|
||||||
|
const nextSearch = { ...search };
|
||||||
|
delete nextSearch.employeeId;
|
||||||
|
setIsEmployeeFormDirty(false);
|
||||||
|
navigate({
|
||||||
|
search: queryString.stringify(nextSearch)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<RbacWrapper action="employees:page">
|
||||||
<RbacWrapper action="employees:page">
|
<div className="shop-employees-layout">
|
||||||
<ShopEmployeesListComponent employees={data ? data.employees : []} loading={loading} />
|
<div className="shop-employees-layout__list">
|
||||||
<ShopEmployeesFormComponent />
|
<ShopEmployeesListComponent
|
||||||
</RbacWrapper>
|
employees={data ? data.employees : []}
|
||||||
</div>
|
loading={loading}
|
||||||
|
onRequestEmployeeChange={navigateToEmployee}
|
||||||
|
selectedEmployeeId={search.employeeId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Drawer
|
||||||
|
open={hasSelectedEmployee}
|
||||||
|
destroyOnHidden
|
||||||
|
placement="right"
|
||||||
|
size={drawerPercentage}
|
||||||
|
onClose={handleDrawerClose}
|
||||||
|
>
|
||||||
|
{hasSelectedEmployee ? (
|
||||||
|
<ShopEmployeesFormComponent form={form} onDirtyChange={setIsEmployeeFormDirty} isDirty={isEmployeeFormDirty} />
|
||||||
|
) : null}
|
||||||
|
</Drawer>
|
||||||
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.shop-employees-layout {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-employees-layout__list {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
304
client/src/components/shop-info/shop-info.color.utils.js
Normal file
304
client/src/components/shop-info/shop-info.color.utils.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* Default translucent card color used for tinting card surfaces when no specific color is provided.
|
||||||
|
* @type {{r: number, g: number, b: number, a: number}}
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TRANSLUCENT_CARD_COLOR = {
|
||||||
|
r: 22,
|
||||||
|
g: 119,
|
||||||
|
b: 255,
|
||||||
|
a: 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a color channel value to two decimal places.
|
||||||
|
* @param value
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const roundColorChannel = (value) => Math.round(value * 100) / 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a tint percentage value to two decimal places.
|
||||||
|
* @param value
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const roundTintPercentage = (value) => Math.round(value * 100) / 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps an alpha value to the range [0, 1] and rounds it to two decimal places.
|
||||||
|
* @param value
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const clampAlpha = (value) => {
|
||||||
|
const numericValue = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isFinite(numericValue)) return 1;
|
||||||
|
if (numericValue <= 0) return 0;
|
||||||
|
if (numericValue >= 1) return 1;
|
||||||
|
|
||||||
|
return numericValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RGB color object to a hexadecimal color string.
|
||||||
|
* @param param0
|
||||||
|
* @param param0.r
|
||||||
|
* @param param0.g
|
||||||
|
* @param param0.b
|
||||||
|
* @returns {`#${string}`}
|
||||||
|
*/
|
||||||
|
const rgbToHex = ({ r, g, b }) =>
|
||||||
|
`#${[r, g, b].map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RGB color object to an HSL color object.
|
||||||
|
* @param param0
|
||||||
|
* @param param0.r
|
||||||
|
* @param param0.g
|
||||||
|
* @param param0.b
|
||||||
|
* @param param0.a
|
||||||
|
* @returns {{h: number, s: number, l: number, a: number}|{h: number, s: number, l: number, a: number}}
|
||||||
|
*/
|
||||||
|
const rgbToHsl = ({ r, g, b, a = 1 }) => {
|
||||||
|
const red = r / 255;
|
||||||
|
const green = g / 255;
|
||||||
|
const blue = b / 255;
|
||||||
|
const max = Math.max(red, green, blue);
|
||||||
|
const min = Math.min(red, green, blue);
|
||||||
|
const delta = max - min;
|
||||||
|
const lightness = (max + min) / 2;
|
||||||
|
|
||||||
|
if (delta === 0) {
|
||||||
|
return { h: 0, s: 0, l: roundColorChannel(lightness), a };
|
||||||
|
}
|
||||||
|
|
||||||
|
const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
||||||
|
let hue;
|
||||||
|
|
||||||
|
switch (max) {
|
||||||
|
case red:
|
||||||
|
hue = (green - blue) / delta + (green < blue ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case green:
|
||||||
|
hue = (blue - red) / delta + 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hue = (red - green) / delta + 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: roundColorChannel(hue * 60),
|
||||||
|
s: roundColorChannel(saturation),
|
||||||
|
l: roundColorChannel(lightness),
|
||||||
|
a
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RGB color object to an HSV color object.
|
||||||
|
* @param param0
|
||||||
|
* @param param0.r
|
||||||
|
* @param param0.g
|
||||||
|
* @param param0.b
|
||||||
|
* @param param0.a
|
||||||
|
* @returns {{h: number, s: number, v: number, a: number}}
|
||||||
|
*/
|
||||||
|
const rgbToHsv = ({ r, g, b, a = 1 }) => {
|
||||||
|
const red = r / 255;
|
||||||
|
const green = g / 255;
|
||||||
|
const blue = b / 255;
|
||||||
|
const max = Math.max(red, green, blue);
|
||||||
|
const min = Math.min(red, green, blue);
|
||||||
|
const delta = max - min;
|
||||||
|
const saturation = max === 0 ? 0 : delta / max;
|
||||||
|
let hue = 0;
|
||||||
|
|
||||||
|
if (delta !== 0) {
|
||||||
|
switch (max) {
|
||||||
|
case red:
|
||||||
|
hue = (green - blue) / delta + (green < blue ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case green:
|
||||||
|
hue = (blue - red) / delta + 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hue = (red - green) / delta + 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: roundColorChannel(hue * 60),
|
||||||
|
s: roundColorChannel(saturation),
|
||||||
|
v: roundColorChannel(max),
|
||||||
|
a
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a comprehensive color value object for a color picker component based on an input RGB color object.
|
||||||
|
* @param rgb
|
||||||
|
* @returns {{hex: `#${string}`, rgb: *, hsl: {h: number, s: number, l: number, a: number}, hsv: {h: number, s: number, v: number, a: number}, oldHue: number, source: string}}
|
||||||
|
*/
|
||||||
|
const buildPickerColorValue = (rgb) => {
|
||||||
|
const hsl = rgbToHsl(rgb);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hex: rgbToHex(rgb),
|
||||||
|
rgb: { ...rgb },
|
||||||
|
hsl,
|
||||||
|
hsv: rgbToHsv(rgb),
|
||||||
|
oldHue: hsl.h,
|
||||||
|
source: "rgb"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default color value object for the color picker component, derived from the default translucent card color.
|
||||||
|
* @type {{hex: `#${string}`, rgb: *, hsl: {h: number, s: number, l: number, a: number}, hsv: {h: number, s: number, v: number, a: number}, oldHue: number, source: string}}
|
||||||
|
*/
|
||||||
|
export const DEFAULT_TRANSLUCENT_PICKER_COLOR = buildPickerColorValue(DEFAULT_TRANSLUCENT_CARD_COLOR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a color string that may be a JSON representation of a color object. If the string is valid JSON and represents
|
||||||
|
* a color, it returns the parsed object; otherwise, it returns the original string.
|
||||||
|
* @param color
|
||||||
|
* @returns {*|string}
|
||||||
|
*/
|
||||||
|
const parseJsonColorString = (color) => {
|
||||||
|
if (typeof color !== "string") return color;
|
||||||
|
|
||||||
|
const trimmedColor = color.trim();
|
||||||
|
if (!trimmedColor.startsWith("{") && !trimmedColor.startsWith("[")) return color;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmedColor);
|
||||||
|
} catch {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a hexadecimal color string (e.g., "#RRGGBB" or "#RRGGBBAA") and returns an object containing the corresponding
|
||||||
|
* RGB color value and alpha transparency. Supports both 3/4-digit and 6/8-digit hex formats.
|
||||||
|
* @param color
|
||||||
|
* @returns {{colorCssValue: string, alpha: number}|null}
|
||||||
|
*/
|
||||||
|
const parseHexColor = (color) => {
|
||||||
|
if (typeof color !== "string") return null;
|
||||||
|
|
||||||
|
const normalizedHex = color.trim().replace(/^#/, "");
|
||||||
|
|
||||||
|
if (![3, 4, 6, 8].includes(normalizedHex.length) || /[^0-9a-f]/i.test(normalizedHex)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedHex =
|
||||||
|
normalizedHex.length <= 4
|
||||||
|
? normalizedHex
|
||||||
|
.split("")
|
||||||
|
.map((character) => `${character}${character}`)
|
||||||
|
.join("")
|
||||||
|
: normalizedHex;
|
||||||
|
|
||||||
|
const hasAlpha = expandedHex.length === 8;
|
||||||
|
const red = Number.parseInt(expandedHex.slice(0, 2), 16);
|
||||||
|
const green = Number.parseInt(expandedHex.slice(2, 4), 16);
|
||||||
|
const blue = Number.parseInt(expandedHex.slice(4, 6), 16);
|
||||||
|
const alpha = hasAlpha ? Number.parseInt(expandedHex.slice(6, 8), 16) / 255 : 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorCssValue: `rgb(${red}, ${green}, ${blue})`,
|
||||||
|
alpha: clampAlpha(alpha)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an RGB or RGBA color string (e.g., "rgb(255, 0, 0)" or "rgba(255, 0, 0, 0.5)") and returns an object
|
||||||
|
* containing the corresponding RGB color value and alpha transparency. Supports both integer and percentage formats for
|
||||||
|
* color channels and alpha.
|
||||||
|
* @param color
|
||||||
|
* @returns {{colorCssValue: string, alpha: number}|null}
|
||||||
|
*/
|
||||||
|
const parseRgbColor = (color) => {
|
||||||
|
if (typeof color !== "string") return null;
|
||||||
|
|
||||||
|
const rgbMatch = color.trim().match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
|
||||||
|
|
||||||
|
if (!rgbMatch) return null;
|
||||||
|
|
||||||
|
const [, red, green, blue, alpha = 1] = rgbMatch;
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorCssValue: `rgb(${red}, ${green}, ${blue})`,
|
||||||
|
alpha: clampAlpha(alpha)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a color input into a consistent descriptor object containing a CSS color value and an alpha transparency
|
||||||
|
* level.
|
||||||
|
* @param color
|
||||||
|
* @returns {{colorCssValue: string, alpha: number}|{colorCssValue: string, alpha: number}|*|{colorCssValue: string, alpha: number}|null}
|
||||||
|
*/
|
||||||
|
const getNormalizedColorDescriptor = (color) => {
|
||||||
|
if (!color) return null;
|
||||||
|
|
||||||
|
const normalizedColor = parseJsonColorString(color);
|
||||||
|
|
||||||
|
if (typeof normalizedColor === "string") {
|
||||||
|
return (
|
||||||
|
parseHexColor(normalizedColor) ||
|
||||||
|
parseRgbColor(normalizedColor) || {
|
||||||
|
colorCssValue: normalizedColor,
|
||||||
|
alpha: 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof normalizedColor === "object" && normalizedColor.rgb) {
|
||||||
|
return getNormalizedColorDescriptor(normalizedColor.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof normalizedColor === "object" && typeof normalizedColor.hex === "string") {
|
||||||
|
return getNormalizedColorDescriptor(normalizedColor.hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof normalizedColor === "object" &&
|
||||||
|
normalizedColor.r !== undefined &&
|
||||||
|
normalizedColor.g !== undefined &&
|
||||||
|
normalizedColor.b !== undefined
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
colorCssValue: `rgb(${normalizedColor.r}, ${normalizedColor.g}, ${normalizedColor.b})`,
|
||||||
|
alpha: clampAlpha(normalizedColor.a)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates CSS styles for tinting card surfaces based on a provided color input. The function normalizes the input
|
||||||
|
* color,
|
||||||
|
* @param color
|
||||||
|
* @returns {{surfaceBg: string, surfaceHeaderBg: string, surfaceBorderColor: string}|{}}
|
||||||
|
*/
|
||||||
|
export const getTintedCardSurfaceStyles = (color) => {
|
||||||
|
const normalizedColor = getNormalizedColorDescriptor(color);
|
||||||
|
if (!normalizedColor?.colorCssValue) return {};
|
||||||
|
|
||||||
|
const tintStrength = clampAlpha(normalizedColor.alpha);
|
||||||
|
if (tintStrength === 0) return {};
|
||||||
|
|
||||||
|
const backgroundTint = roundTintPercentage(10 * tintStrength);
|
||||||
|
const headerTint = roundTintPercentage(18 * tintStrength);
|
||||||
|
const borderTint = roundTintPercentage(30 * tintStrength);
|
||||||
|
|
||||||
|
return {
|
||||||
|
surfaceBg: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${backgroundTint}%, var(--imex-form-surface))`,
|
||||||
|
surfaceHeaderBg: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${headerTint}%, var(--imex-form-surface-head))`,
|
||||||
|
surfaceBorderColor: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${borderTint}%, var(--imex-form-surface-border))`
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { getTintedCardSurfaceStyles } from "./shop-info.color.utils";
|
||||||
|
|
||||||
|
describe("shop info color utilities", () => {
|
||||||
|
it("scales card tint intensity with alpha for plain rgba values", () => {
|
||||||
|
expect(
|
||||||
|
getTintedCardSurfaceStyles({
|
||||||
|
r: 22,
|
||||||
|
g: 119,
|
||||||
|
b: 255,
|
||||||
|
a: 0.5
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
surfaceBg: "color-mix(in srgb, rgb(22, 119, 255) 5%, var(--imex-form-surface))",
|
||||||
|
surfaceHeaderBg: "color-mix(in srgb, rgb(22, 119, 255) 9%, var(--imex-form-surface-head))",
|
||||||
|
surfaceBorderColor: "color-mix(in srgb, rgb(22, 119, 255) 15%, var(--imex-form-surface-border))"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns no tint when the selected color alpha is zero", () => {
|
||||||
|
expect(
|
||||||
|
getTintedCardSurfaceStyles({
|
||||||
|
hex: "#1677ff",
|
||||||
|
rgb: {
|
||||||
|
r: 22,
|
||||||
|
g: 119,
|
||||||
|
b: 255,
|
||||||
|
a: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports legacy JSON-stringified picker values", () => {
|
||||||
|
expect(
|
||||||
|
getTintedCardSurfaceStyles(
|
||||||
|
JSON.stringify({
|
||||||
|
rgb: {
|
||||||
|
r: 255,
|
||||||
|
g: 0,
|
||||||
|
b: 0,
|
||||||
|
a: 0.25
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
surfaceBg: "color-mix(in srgb, rgb(255, 0, 0) 2.5%, var(--imex-form-surface))",
|
||||||
|
surfaceHeaderBg: "color-mix(in srgb, rgb(255, 0, 0) 4.5%, var(--imex-form-surface-head))",
|
||||||
|
surfaceBorderColor: "color-mix(in srgb, rgb(255, 0, 0) 7.5%, var(--imex-form-surface-border))"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
import { useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -21,6 +22,7 @@ import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycen
|
|||||||
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
import ShopInfoRoGuard from "./shop-info.roguard.component";
|
||||||
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
||||||
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
||||||
|
import ShopInfoSectionNavigator from "./shop-info.section-navigator.component.jsx";
|
||||||
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
||||||
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
import ShopInfoTaskPresets from "./shop-info.task-presets.component";
|
||||||
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
import ShopInfoIntellipay from "./shop-intellipay-config.component";
|
||||||
@@ -33,7 +35,7 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
||||||
|
|
||||||
export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
export function ShopInfoComponent({ bodyshop, form, saveLoading, isDirty }) {
|
||||||
const {
|
const {
|
||||||
treatments: { CriticalPartsScanning, Enhanced_Payroll }
|
treatments: { CriticalPartsScanning, Enhanced_Payroll }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -47,6 +49,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
|
const tabsRef = useRef(null);
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
@@ -154,23 +157,35 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
|||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
];
|
];
|
||||||
|
const activeTabKey = search.subtab || tabItems[0]?.key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
title={<ShopInfoSectionNavigator tabsRef={tabsRef} activeTabKey={activeTabKey} />}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" loading={saveLoading} onClick={() => form.submit()} id="shop-info-save-button">
|
<Button
|
||||||
{t("general.actions.save")}
|
type="primary"
|
||||||
|
disabled={!isDirty || saveLoading}
|
||||||
|
loading={saveLoading}
|
||||||
|
onClick={() => form.submit()}
|
||||||
|
id="shop-info-save-button"
|
||||||
|
style={{ minWidth: 210 }}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.save_shop_information")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tabs
|
<div ref={tabsRef}>
|
||||||
defaultActiveKey={search.subtab}
|
<Tabs
|
||||||
onChange={(key) =>
|
activeKey={activeTabKey}
|
||||||
history({
|
onChange={(key) =>
|
||||||
search: `?tab=${search.tab}&subtab=${key}`
|
history({
|
||||||
})
|
search: `?tab=${search.tab}&subtab=${key}`
|
||||||
}
|
})
|
||||||
items={tabItems}
|
}
|
||||||
/>
|
items={tabItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Typography } from "antd";
|
import { Card } from "antd";
|
||||||
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";
|
||||||
@@ -15,9 +15,8 @@ function ShopInfoConsentComponent({ bodyshop }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card title={t("settings.title")}>
|
||||||
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
<PhoneNumberConsentList bodyshop={bodyshop} />
|
||||||
{<PhoneNumberConsentList bodyshop={bodyshop} />}
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { Form } from "antd";
|
import { Form } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
@@ -15,6 +15,7 @@ import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservat
|
|||||||
export default function ShopInfoContainer() {
|
export default function ShopInfoContainer() {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isShopInfoDirty, setIsShopInfoDirty] = useState(false);
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
const [updateBodyshop] = useMutation(UPDATE_SHOP);
|
const [updateBodyshop] = useMutation(UPDATE_SHOP);
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, {
|
const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, {
|
||||||
@@ -33,7 +34,10 @@ export default function ShopInfoContainer() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
const combinedFeatureConfig = useMemo(
|
||||||
|
() => combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Use form data preservation for all shop-info features
|
// Use form data preservation for all shop-info features
|
||||||
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||||
@@ -51,7 +55,10 @@ export default function ShopInfoContainer() {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification.success({ title: t("bodyshop.successes.save") });
|
notification.success({ title: t("bodyshop.successes.save") });
|
||||||
refetch().then(() => form.resetFields());
|
refetch().then(() => {
|
||||||
|
form.resetFields();
|
||||||
|
setIsShopInfoDirty(false);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification.error({
|
notification.error({
|
||||||
@@ -66,6 +73,7 @@ export default function ShopInfoContainer() {
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
// After reset, re-apply hidden field preservation so values aren't wiped
|
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||||
preserveHiddenFormData();
|
preserveHiddenFormData();
|
||||||
|
setIsShopInfoDirty(false);
|
||||||
}, [data, form, preserveHiddenFormData]);
|
}, [data, form, preserveHiddenFormData]);
|
||||||
|
|
||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
@@ -76,6 +84,9 @@ export default function ShopInfoContainer() {
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
onValuesChange={() => {
|
||||||
|
setIsShopInfoDirty(form.isFieldsTouched());
|
||||||
|
}}
|
||||||
initialValues={
|
initialValues={
|
||||||
data
|
data
|
||||||
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
|
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
|
||||||
@@ -99,8 +110,8 @@ export default function ShopInfoContainer() {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormsFieldChanged form={form} />
|
<FormsFieldChanged form={form} onDirtyChange={setIsShopInfoDirty} />
|
||||||
<ShopInfoComponent form={form} saveLoading={saveLoading} />
|
<ShopInfoComponent form={form} saveLoading={saveLoading} isDirty={isShopInfoDirty} />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,19 @@ import styled from "styled-components";
|
|||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import ConfigFormTypes from "../config-form-components/config-form-types";
|
import ConfigFormTypes from "../config-form-components/config-form-types";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
|
|
||||||
const SelectorDiv = styled.div`
|
const SelectorDiv = styled.div`
|
||||||
.ant-form-item .ant-select {
|
.ant-form-item .ant-select {
|
||||||
@@ -19,306 +31,386 @@ export default function ShopInfoIntakeChecklistComponent({ form }) {
|
|||||||
const TemplateListGenerated = TemplateList();
|
const TemplateListGenerated = TemplateList();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.intakechecklist")} id="intakechecklist">
|
|
||||||
<Form.List name={["intakechecklist", "form"]}>
|
|
||||||
{(fields, { add, remove, move }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item key={field.key}>
|
|
||||||
<LayoutFormRow noDivider>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.name")}
|
|
||||||
key={`${index}name`}
|
|
||||||
name={[field.name, "name"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.type")}
|
|
||||||
key={`${index}type`}
|
|
||||||
name={[field.name, "type"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.label")}
|
|
||||||
key={`${index}label`}
|
|
||||||
name={[field.name, "label"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
|
||||||
{() => {
|
|
||||||
if (form.getFieldValue(["intakechecklist", "form", index, "type"]) !== "slider") return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.min")}
|
|
||||||
key={`${index}min`}
|
|
||||||
name={[field.name, "min"]}
|
|
||||||
dependencies={[[field.name, "type"]]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.max")}
|
|
||||||
key={`${index}max`}
|
|
||||||
name={[field.name, "max"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.required")}
|
|
||||||
key={`${index}required`}
|
|
||||||
name={[field.name, "required"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</LayoutFormRow>
|
|
||||||
<SelectorDiv>
|
<SelectorDiv>
|
||||||
<Form.Item
|
<LayoutFormRow header={t("bodyshop.labels.intake_delivery")} id="intake-delivery">
|
||||||
name={["intakechecklist", "templates"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.intake.templates")}
|
col={{ xs: 24, sm: 24, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
rules={[
|
name={["intakechecklist", "templates"]}
|
||||||
{
|
label={t("bodyshop.fields.intake.templates")}
|
||||||
required: true,
|
rules={[
|
||||||
//message: t("general.validation.required"),
|
{
|
||||||
type: "array"
|
required: true,
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
type: "array"
|
||||||
>
|
}
|
||||||
<Select
|
]}
|
||||||
mode="multiple"
|
>
|
||||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
<Select
|
||||||
value: TemplateListGenerated[i].key,
|
mode="multiple"
|
||||||
label: TemplateListGenerated[i].title
|
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||||
}))}
|
value: TemplateListGenerated[i].key,
|
||||||
/>
|
label: TemplateListGenerated[i].title
|
||||||
</Form.Item>
|
}))}
|
||||||
<Form.Item
|
/>
|
||||||
name={["intakechecklist", "next_contact_hours"]}
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.intake.next_contact_hours")}
|
<Form.Item
|
||||||
>
|
col={{ xs: 24, sm: 24, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
<InputNumber min={0} precision={0} />
|
name={["deliverchecklist", "templates"]}
|
||||||
</Form.Item>
|
label={t("bodyshop.fields.deliver.templates")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
options={Object.keys(TemplateListGenerated).map((i) => ({
|
||||||
|
value: TemplateListGenerated[i].key,
|
||||||
|
label: TemplateListGenerated[i].title
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 10, md: 8, lg: 8, xl: 8, xxl: 8 }}
|
||||||
|
name={["intakechecklist", "next_contact_hours"]}
|
||||||
|
label={t("bodyshop.fields.intake.next_contact_hours")}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} precision={0} suffix="hrs" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
col={{ xs: 24, sm: 14, md: 16, lg: 16, xl: 16, xxl: 16 }}
|
||||||
|
name={["deliverchecklist", "actual_delivery"]}
|
||||||
|
label={t("bodyshop.fields.deliver.require_actual_delivery_date")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
</SelectorDiv>
|
</SelectorDiv>
|
||||||
|
<Form.List name={["intakechecklist", "form"]}>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.deliverchecklist")} id="deliverchecklist">
|
{(fields, { add, remove, move }) => {
|
||||||
<Form.List name={["deliverchecklist", "form"]}>
|
return (
|
||||||
{(fields, { add, remove, move }) => {
|
<LayoutFormRow
|
||||||
return (
|
header={t("bodyshop.labels.intakechecklist")}
|
||||||
|
id="intakechecklist"
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="add-intake-checklist-item"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.add_intake_checklist_item")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.length === 0 ? (
|
||||||
<Form.Item key={field.key}>
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_intake_checklist_item")} />
|
||||||
<LayoutFormRow noDivider>
|
) : (
|
||||||
<Form.Item
|
fields.map((field, index) => {
|
||||||
label={t("jobs.fields.intake.name")}
|
return (
|
||||||
key={`${index}named`}
|
<Form.Item noStyle key={field.key}>
|
||||||
name={[field.name, "name"]}
|
<InlineValidatedFormRow
|
||||||
rules={[
|
form={form}
|
||||||
{
|
errorNames={[["intakechecklist", "form", field.name, "name"]]}
|
||||||
required: true
|
noDivider
|
||||||
//message: t("general.validation.required"),
|
title={
|
||||||
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("jobs.fields.intake.name")}
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div>
|
||||||
|
<Form.Item noStyle name={[field.name, "required"]} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
]}
|
wrapTitle
|
||||||
>
|
extra={
|
||||||
<Input />
|
<Space align="center" size="small">
|
||||||
</Form.Item>
|
<Button
|
||||||
|
type="text"
|
||||||
<Form.Item
|
danger
|
||||||
label={t("jobs.fields.intake.type")}
|
icon={<DeleteFilled />}
|
||||||
key={`${index}typed`}
|
onClick={() => {
|
||||||
name={[field.name, "type"]}
|
remove(field.name);
|
||||||
rules={[
|
}}
|
||||||
{
|
/>
|
||||||
required: true
|
<FormListMoveArrows
|
||||||
//message: t("general.validation.required"),
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
]}
|
>
|
||||||
>
|
<Form.Item
|
||||||
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
label={t("jobs.fields.intake.type")}
|
||||||
</Form.Item>
|
key={`${index}type`}
|
||||||
|
name={[field.name, "type"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.label")}
|
||||||
|
key={`${index}label`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item shouldUpdate>
|
||||||
label={t("jobs.fields.intake.label")}
|
{() => {
|
||||||
key={`${index}labeld`}
|
if (form.getFieldValue(["intakechecklist", "form", index, "type"]) !== "slider")
|
||||||
name={[field.name, "label"]}
|
return null;
|
||||||
rules={[
|
return (
|
||||||
{
|
<>
|
||||||
required: true
|
<Form.Item
|
||||||
//message: t("general.validation.required"),
|
label={t("jobs.fields.intake.min")}
|
||||||
}
|
key={`${index}min`}
|
||||||
]}
|
name={[field.name, "min"]}
|
||||||
>
|
dependencies={[[field.name, "type"]]}
|
||||||
<Input />
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.max")}
|
||||||
|
key={`${index}max`}
|
||||||
|
name={[field.name, "max"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
);
|
||||||
<Form.Item shouldUpdate>
|
})
|
||||||
{() => {
|
)}
|
||||||
if (form.getFieldValue(["deliverchecklist", "form", index, "type"]) !== "slider") return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.min")}
|
|
||||||
key={`${index}mind`}
|
|
||||||
name={[field.name, "min"]}
|
|
||||||
dependencies={[[field.name, "type"]]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: form.getFieldValue([field.name, "type"]) === "slider"
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.max")}
|
|
||||||
key={`${index}maxd`}
|
|
||||||
name={[field.name, "max"]}
|
|
||||||
dependencies={[[field.name, "type"]]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: form.getFieldValue([field.name, "type"]) === "slider"
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.intake.required")}
|
|
||||||
key={`${index}requiredd`}
|
|
||||||
name={[field.name, "required"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</LayoutFormRow>
|
||||||
}}
|
);
|
||||||
</Form.List>
|
}}
|
||||||
</LayoutFormRow>
|
</Form.List>
|
||||||
<SelectorDiv>
|
<Form.List name={["deliverchecklist", "form"]}>
|
||||||
<Form.Item
|
{(fields, { add, remove, move }) => {
|
||||||
name={["deliverchecklist", "templates"]}
|
return (
|
||||||
label={t("bodyshop.fields.deliver.templates")}
|
<LayoutFormRow
|
||||||
rules={[
|
header={t("bodyshop.labels.deliverchecklist")}
|
||||||
{
|
id="deliverchecklist"
|
||||||
required: true,
|
actions={[
|
||||||
//message: t("general.validation.required"),
|
<Button
|
||||||
type: "array"
|
key="add-delivery-checklist-item"
|
||||||
}
|
type="primary"
|
||||||
]}
|
block
|
||||||
>
|
onClick={() => {
|
||||||
<Select
|
add();
|
||||||
mode="multiple"
|
}}
|
||||||
options={Object.keys(TemplateListGenerated).map((i) => ({
|
>
|
||||||
value: TemplateListGenerated[i].key,
|
{t("bodyshop.actions.add_delivery_checklist_item")}
|
||||||
label: TemplateListGenerated[i].title
|
</Button>
|
||||||
}))}
|
]}
|
||||||
/>
|
>
|
||||||
</Form.Item>
|
<div>
|
||||||
<Form.Item
|
{fields.length === 0 ? (
|
||||||
name={["deliverchecklist", "actual_delivery"]}
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_delivery_checklist_item")} />
|
||||||
label={t("bodyshop.fields.deliver.require_actual_delivery_date")}
|
) : (
|
||||||
rules={[
|
fields.map((field, index) => {
|
||||||
{
|
return (
|
||||||
required: true
|
<Form.Item noStyle key={field.key}>
|
||||||
//message: t("general.validation.required"),
|
<InlineValidatedFormRow
|
||||||
}
|
form={form}
|
||||||
]}
|
errorNames={[["deliverchecklist", "form", field.name, "name"]]}
|
||||||
>
|
noDivider
|
||||||
<Switch />
|
title={
|
||||||
</Form.Item>
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
</SelectorDiv>
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.name")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("jobs.fields.intake.name")}
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.intake.required")}</div>
|
||||||
|
<Form.Item noStyle name={[field.name, "required"]} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.type")}
|
||||||
|
key={`${index}typed`}
|
||||||
|
name={[field.name, "type"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select options={Object.keys(ConfigFormTypes).map((i) => ({ value: i, label: i }))} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.label")}
|
||||||
|
key={`${index}labeld`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
if (form.getFieldValue(["deliverchecklist", "form", index, "type"]) !== "slider")
|
||||||
|
return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.min")}
|
||||||
|
key={`${index}mind`}
|
||||||
|
name={[field.name, "min"]}
|
||||||
|
dependencies={[[field.name, "type"]]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue([field.name, "type"]) === "slider"
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.intake.max")}
|
||||||
|
key={`${index}maxd`}
|
||||||
|
name={[field.name, "max"]}
|
||||||
|
dependencies={[[field.name, "type"]]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue([field.name, "type"]) === "slider"
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,344 +3,392 @@ import { Button, Form, Input, Space } from "antd";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
|
|
||||||
export default function ShopInfoLaborRates() {
|
export default function ShopInfoLaborRates() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
||||||
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput prefix="$" min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.laborrates")}>
|
<Form.List name={["md_labor_rates"]}>
|
||||||
<Form.List name={["md_labor_rates"]}>
|
{(fields, { add, remove, move }) => {
|
||||||
{(fields, { add, remove, move }) => {
|
return (
|
||||||
return (
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.laborrates")}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="add-labor-rate"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.newlaborrate")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.length === 0 ? (
|
||||||
<Form.Item key={field.key}>
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.newlaborrate")} />
|
||||||
<LayoutFormRow noDivider={index === 0}>
|
) : (
|
||||||
<Form.Item
|
fields.map((field, index) => {
|
||||||
label={t("jobs.fields.labor_rate_desc")}
|
return (
|
||||||
key={`${index}rate_label`}
|
<Form.Item noStyle key={field.key}>
|
||||||
name={[field.name, "rate_label"]}
|
<InlineValidatedFormRow
|
||||||
rules={[
|
form={form}
|
||||||
{
|
errorNames={[["md_labor_rates", field.name, "rate_label"]]}
|
||||||
required: true
|
noDivider={index === 0}
|
||||||
//message: t("general.validation.required"),
|
title={
|
||||||
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("jobs.fields.labor_rate_desc")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "rate_label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("jobs.fields.labor_rate_desc")}
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
]}
|
wrapTitle
|
||||||
>
|
extra={
|
||||||
<Input />
|
<Space align="center" size="small">
|
||||||
</Form.Item>
|
<Button
|
||||||
<Form.Item
|
type="text"
|
||||||
label={t("jobs.fields.rate_laa")}
|
danger
|
||||||
key={`${index}rate_laa`}
|
icon={<DeleteFilled />}
|
||||||
name={[field.name, "rate_laa"]}
|
onClick={() => {
|
||||||
rules={[
|
remove(field.name);
|
||||||
{
|
}}
|
||||||
required: true
|
/>
|
||||||
//message: t("general.validation.required"),
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
]}
|
>
|
||||||
>
|
<Form.Item
|
||||||
<CurrencyInput min={0} />
|
label={t("jobs.fields.rate_laa")}
|
||||||
</Form.Item>
|
key={`${index}rate_laa`}
|
||||||
<Form.Item
|
name={[field.name, "rate_laa"]}
|
||||||
label={t("jobs.fields.rate_lab")}
|
rules={[
|
||||||
key={`${index}rate_lab`}
|
{
|
||||||
name={[field.name, "rate_lab"]}
|
required: true
|
||||||
rules={[
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lab")}
|
||||||
|
key={`${index}rate_lab`}
|
||||||
|
name={[field.name, "rate_lab"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lad")}
|
||||||
|
key={`${index}rate_lad`}
|
||||||
|
name={[field.name, "rate_lad"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lae")}
|
||||||
|
key={`${index}rate_lae`}
|
||||||
|
name={[field.name, "rate_lae"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_laf")}
|
||||||
|
key={`${index}rate_laf`}
|
||||||
|
name={[field.name, "rate_laf"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lag")}
|
||||||
|
key={`${index}rate_lag`}
|
||||||
|
name={[field.name, "rate_lag"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lam")}
|
||||||
|
key={`${index}rate_lam`}
|
||||||
|
name={[field.name, "rate_lam"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_lar")}
|
||||||
|
key={`${index}rate_lar`}
|
||||||
|
name={[field.name, "rate_lar"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_las")}
|
||||||
|
key={`${index}rate_las`}
|
||||||
|
name={[field.name, "rate_las"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_la1")}
|
||||||
|
key={`${index}rate_la1`}
|
||||||
|
name={[field.name, "rate_la1"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_la2")}
|
||||||
|
key={`${index}rate_la2`}
|
||||||
|
name={[field.name, "rate_la2"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_la3")}
|
||||||
|
key={`${index}rate_la3`}
|
||||||
|
name={[field.name, "rate_la3"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_la4")}
|
||||||
|
key={`${index}rate_la4`}
|
||||||
|
name={[field.name, "rate_la4"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_mash")}
|
||||||
|
key={`${index}rate_mash`}
|
||||||
|
name={[field.name, "rate_mash"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_mapa")}
|
||||||
|
key={`${index}rate_mapa`}
|
||||||
|
name={[field.name, "rate_mapa"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_ma2s")}
|
||||||
|
key={`${index}rate_ma2s`}
|
||||||
|
name={[field.name, "rate_ma2s"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_ma3s")}
|
||||||
|
key={`${index}rate_ma3s`}
|
||||||
|
name={[field.name, "rate_ma3s"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
{
|
{
|
||||||
required: true
|
// <Form.Item
|
||||||
//message: t("general.validation.required"),
|
// label={t("jobs.fields.rate_mabl")}
|
||||||
|
// key={`${index}rate_mabl`}
|
||||||
|
// name={[field.name, "rate_mabl"]}
|
||||||
|
// rules={[
|
||||||
|
// {
|
||||||
|
// required: true,
|
||||||
|
// //message: t("general.validation.required"),
|
||||||
|
// },
|
||||||
|
// ]}
|
||||||
|
// >
|
||||||
|
// <CurrencyInput min={0} />
|
||||||
|
// </Form.Item>
|
||||||
|
// <Form.Item
|
||||||
|
// label={t("jobs.fields.rate_macs")}
|
||||||
|
// key={`${index}rate_macs`}
|
||||||
|
// name={[field.name, "rate_macs"]}
|
||||||
|
// rules={[
|
||||||
|
// {
|
||||||
|
// required: true,
|
||||||
|
// //message: t("general.validation.required"),
|
||||||
|
// },
|
||||||
|
// ]}
|
||||||
|
// >
|
||||||
|
// <CurrencyInput min={0} />
|
||||||
|
// </Form.Item>
|
||||||
}
|
}
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_matd")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_matd`}
|
||||||
|
name={[field.name, "rate_matd"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_mahw")}
|
||||||
|
key={`${index}rate_mahw`}
|
||||||
|
name={[field.name, "rate_mahw"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput prefix="$" min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
);
|
||||||
label={t("jobs.fields.rate_lad")}
|
})
|
||||||
key={`${index}rate_lad`}
|
)}
|
||||||
name={[field.name, "rate_lad"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_lae")}
|
|
||||||
key={`${index}rate_lae`}
|
|
||||||
name={[field.name, "rate_lae"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_laf")}
|
|
||||||
key={`${index}rate_laf`}
|
|
||||||
name={[field.name, "rate_laf"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_lag")}
|
|
||||||
key={`${index}rate_lag`}
|
|
||||||
name={[field.name, "rate_lag"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_lam")}
|
|
||||||
key={`${index}rate_lam`}
|
|
||||||
name={[field.name, "rate_lam"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_lar")}
|
|
||||||
key={`${index}rate_lar`}
|
|
||||||
name={[field.name, "rate_lar"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_las")}
|
|
||||||
key={`${index}rate_las`}
|
|
||||||
name={[field.name, "rate_las"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_la1")}
|
|
||||||
key={`${index}rate_la1`}
|
|
||||||
name={[field.name, "rate_la1"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_la2")}
|
|
||||||
key={`${index}rate_la2`}
|
|
||||||
name={[field.name, "rate_la2"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_la3")}
|
|
||||||
key={`${index}rate_la3`}
|
|
||||||
name={[field.name, "rate_la3"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_la4")}
|
|
||||||
key={`${index}rate_la4`}
|
|
||||||
name={[field.name, "rate_la4"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_mash")}
|
|
||||||
key={`${index}rate_mash`}
|
|
||||||
name={[field.name, "rate_mash"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_mapa")}
|
|
||||||
key={`${index}rate_mapa`}
|
|
||||||
name={[field.name, "rate_mapa"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_ma2s")}
|
|
||||||
key={`${index}rate_ma2s`}
|
|
||||||
name={[field.name, "rate_ma2s"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_ma3s")}
|
|
||||||
key={`${index}rate_ma3s`}
|
|
||||||
name={[field.name, "rate_ma3s"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
{
|
|
||||||
// <Form.Item
|
|
||||||
// label={t("jobs.fields.rate_mabl")}
|
|
||||||
// key={`${index}rate_mabl`}
|
|
||||||
// name={[field.name, "rate_mabl"]}
|
|
||||||
// rules={[
|
|
||||||
// {
|
|
||||||
// required: true,
|
|
||||||
// //message: t("general.validation.required"),
|
|
||||||
// },
|
|
||||||
// ]}
|
|
||||||
// >
|
|
||||||
// <CurrencyInput min={0} />
|
|
||||||
// </Form.Item>
|
|
||||||
// <Form.Item
|
|
||||||
// label={t("jobs.fields.rate_macs")}
|
|
||||||
// key={`${index}rate_macs`}
|
|
||||||
// name={[field.name, "rate_macs"]}
|
|
||||||
// rules={[
|
|
||||||
// {
|
|
||||||
// required: true,
|
|
||||||
// //message: t("general.validation.required"),
|
|
||||||
// },
|
|
||||||
// ]}
|
|
||||||
// >
|
|
||||||
// <CurrencyInput min={0} />
|
|
||||||
// </Form.Item>
|
|
||||||
}
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_matd")}
|
|
||||||
key={`${index}rate_matd`}
|
|
||||||
name={[field.name, "rate_matd"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("jobs.fields.rate_mahw")}
|
|
||||||
key={`${index}rate_mahw`}
|
|
||||||
name={[field.name, "rate_mahw"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyInput min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Space>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.newlaborrate")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</LayoutFormRow>
|
||||||
}}
|
);
|
||||||
</Form.List>
|
}}
|
||||||
</LayoutFormRow>
|
</Form.List>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Form, Typography } from "antd";
|
import { Form, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
@@ -11,43 +12,45 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
|||||||
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
|
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow header={t("bodyshop.labels.notification_options")}>
|
||||||
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
<div>
|
||||||
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
<Paragraph>{t("bodyshop.fields.notifications.description")}</Paragraph>
|
||||||
{employeeOptions.length > 0 ? (
|
<Text type="secondary">{t("bodyshop.labels.notifications.followers")}</Text>
|
||||||
<Form.Item
|
{employeeOptions.length > 0 ? (
|
||||||
normalize={(value) => (value || []).filter((id) => typeof id === "string" && id.trim() !== "")}
|
<Form.Item
|
||||||
name="notification_followers"
|
normalize={(value) => (value || []).filter((id) => typeof id === "string" && id.trim() !== "")}
|
||||||
rules={[
|
name="notification_followers"
|
||||||
{
|
rules={[
|
||||||
type: "array",
|
{
|
||||||
message: t("general.validation.array")
|
type: "array",
|
||||||
},
|
message: t("general.validation.array")
|
||||||
{
|
},
|
||||||
validator: async (_, value) => {
|
{
|
||||||
if (!value || value.length === 0) {
|
validator: async (_, value) => {
|
||||||
return Promise.resolve(); // Allow empty array
|
if (!value || value.length === 0) {
|
||||||
|
return Promise.resolve(); // Allow empty array
|
||||||
|
}
|
||||||
|
const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
|
||||||
|
if (hasInvalid) {
|
||||||
|
return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === "");
|
|
||||||
if (hasInvalid) {
|
|
||||||
return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers")));
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
}
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<EmployeeSearchSelectComponent
|
||||||
<EmployeeSearchSelectComponent
|
style={{ minWidth: "100%" }}
|
||||||
style={{ minWidth: "100%" }}
|
mode="multiple"
|
||||||
mode="multiple"
|
options={employeeOptions}
|
||||||
options={employeeOptions}
|
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
||||||
placeholder={t("bodyshop.fields.notifications.placeholder")}
|
showEmail={true}
|
||||||
showEmail={true}
|
/>
|
||||||
/>
|
</Form.Item>
|
||||||
</Form.Item>
|
) : (
|
||||||
) : (
|
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
|
||||||
<Text type="secondary">{t("bodyshop.fields.no_employees_available")}</Text>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,19 @@ import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
|
|
||||||
const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
|
const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
|
||||||
@@ -68,195 +80,223 @@ export default function ShopInfoPartsScan({ form }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
|
<Form.List name={["md_parts_scan"]}>
|
||||||
<Form.List name={["md_parts_scan"]}>
|
{(fields, { add, remove, move }) => (
|
||||||
{(fields, { add, remove, move }) => (
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.md_parts_scan")}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="add-parts-scan-rule"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={() =>
|
||||||
|
add({
|
||||||
|
field: "line_desc",
|
||||||
|
operation: "contains",
|
||||||
|
mark_critical: true,
|
||||||
|
caseInsensitive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.addpartsrule")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => {
|
{fields.length === 0 ? (
|
||||||
const selectedField = watchedFields?.[index]?.field || "line_desc";
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.addpartsrule")} />
|
||||||
const fieldType = getFieldType(selectedField);
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
const selectedField = watchedFields?.[index]?.field || "line_desc";
|
||||||
|
const fieldType = getFieldType(selectedField);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={field.key}>
|
<Form.Item noStyle key={field.key}>
|
||||||
<Row gutter={[16, 16]} align="middle">
|
<InlineValidatedFormRow
|
||||||
{/* Select Field */}
|
form={form}
|
||||||
<Col span={6}>
|
errorNames={[["md_parts_scan", field.name, "field"]]}
|
||||||
<Form.Item
|
noDivider
|
||||||
label={t("bodyshop.fields.md_parts_scan.field")}
|
title={
|
||||||
name={[field.name, "field"]}
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
rules={[
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
{
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
required: true,
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.md_parts_scan.field")}</div>
|
||||||
message: t("general.validation.required", {
|
<Form.Item
|
||||||
label: t("bodyshop.fields.md_parts_scan.field")
|
noStyle
|
||||||
})
|
name={[field.name, "field"]}
|
||||||
}
|
rules={[
|
||||||
]}
|
{
|
||||||
>
|
required: true,
|
||||||
<Select
|
message: t("general.validation.required", {
|
||||||
options={fieldSelectOptions}
|
label: t("bodyshop.fields.md_parts_scan.field")
|
||||||
onChange={() => {
|
})
|
||||||
form.setFields([
|
}
|
||||||
{ name: ["md_parts_scan", index, "operation"], value: "contains" },
|
]}
|
||||||
{ name: ["md_parts_scan", index, "value"], value: undefined }
|
>
|
||||||
]);
|
<Select
|
||||||
}}
|
options={fieldSelectOptions}
|
||||||
/>
|
onChange={() => {
|
||||||
</Form.Item>
|
form.setFields([
|
||||||
</Col>
|
{ name: ["md_parts_scan", index, "operation"], value: "contains" },
|
||||||
|
{ name: ["md_parts_scan", index, "value"], value: undefined }
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
selector: INLINE_TITLE_INPUT_STYLE
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
{fieldType === "string" && (
|
||||||
|
<>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>
|
||||||
|
{t("bodyshop.fields.md_parts_scan.caseInsensitive")}
|
||||||
|
</div>
|
||||||
|
<Form.Item noStyle name={[field.name, "caseInsensitive"]} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_SWITCH_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>
|
||||||
|
{t("bodyshop.fields.md_parts_scan.mark_critical")}
|
||||||
|
</div>
|
||||||
|
<Form.Item noStyle name={[field.name, "mark_critical"]} valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row gutter={[16, 16]} align="middle">
|
||||||
|
{/* Operation */}
|
||||||
|
{fieldType !== "predefined" && fieldType && (
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_parts_scan.operation")}
|
||||||
|
name={[field.name, "operation"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t("general.validation.required", {
|
||||||
|
label: t("bodyshop.fields.md_parts_scan.operation")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select options={operationOptions[fieldType]} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Operation */}
|
{/* Value */}
|
||||||
{fieldType !== "predefined" && fieldType && (
|
{fieldType && (
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_parts_scan.operation")}
|
label={t("bodyshop.fields.md_parts_scan.value")}
|
||||||
name={[field.name, "operation"]}
|
name={[field.name, "value"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("general.validation.required", {
|
message: t("general.validation.required", {
|
||||||
label: t("bodyshop.fields.md_parts_scan.operation")
|
label: t("bodyshop.fields.md_parts_scan.value")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select options={operationOptions[fieldType]} />
|
{fieldType === "predefined" ? (
|
||||||
</Form.Item>
|
<Select
|
||||||
</Col>
|
options={
|
||||||
)}
|
selectedField === "part_type"
|
||||||
|
? predefinedPartTypes.map((type) => ({
|
||||||
|
label: type,
|
||||||
|
value: type
|
||||||
|
}))
|
||||||
|
: predefinedModLbrTypes.map((type) => ({
|
||||||
|
label: type,
|
||||||
|
value: type
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Value */}
|
{/* Update Field */}
|
||||||
{fieldType && (
|
<Col span={4}>
|
||||||
<Col span={6}>
|
<Form.Item
|
||||||
<Form.Item
|
label={t("bodyshop.fields.md_parts_scan.update_field")}
|
||||||
label={t("bodyshop.fields.md_parts_scan.value")}
|
name={[field.name, "update_field"]}
|
||||||
name={[field.name, "value"]}
|
>
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t("general.validation.required", {
|
|
||||||
label: t("bodyshop.fields.md_parts_scan.value")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{fieldType === "predefined" ? (
|
|
||||||
<Select
|
<Select
|
||||||
options={
|
options={fieldSelectOptions}
|
||||||
selectedField === "part_type"
|
allowClear
|
||||||
? predefinedPartTypes.map((type) => ({
|
onClear={() =>
|
||||||
label: type,
|
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
|
||||||
value: type
|
|
||||||
}))
|
|
||||||
: predefinedModLbrTypes.map((type) => ({
|
|
||||||
label: type,
|
|
||||||
value: type
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Update Field */}
|
||||||
|
<Col span={4}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_parts_scan.update_value")}
|
||||||
|
name={[field.name, "update_value"]}
|
||||||
|
dependencies={[["md_parts_scan", index, "update_field"]]}
|
||||||
|
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
|
||||||
|
message: t("general.validation.required", {
|
||||||
|
label: t("bodyshop.fields.md_parts_scan.update_value")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
)}
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
)}
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
{/* Case Sensitivity */}
|
);
|
||||||
{fieldType === "string" && (
|
})
|
||||||
<Col span={4}>
|
)}
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
|
|
||||||
name={[field.name, "caseInsensitive"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
labelCol={{ span: 14 }}
|
|
||||||
wrapperCol={{ span: 10 }}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mark Line as Critical */}
|
|
||||||
<Col span={4}>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_parts_scan.mark_critical")}
|
|
||||||
name={[field.name, "mark_critical"]}
|
|
||||||
valuePropName="checked"
|
|
||||||
labelCol={{ span: 14 }}
|
|
||||||
wrapperCol={{ span: 10 }}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Update Field */}
|
|
||||||
<Col span={4}>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_parts_scan.update_field")}
|
|
||||||
name={[field.name, "update_field"]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={fieldSelectOptions}
|
|
||||||
allowClear
|
|
||||||
onClear={() =>
|
|
||||||
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Update Field */}
|
|
||||||
<Col span={4}>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_parts_scan.update_value")}
|
|
||||||
name={[field.name, "update_value"]}
|
|
||||||
dependencies={[["md_parts_scan", index, "update_field"]]}
|
|
||||||
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
|
|
||||||
message: t("general.validation.required", {
|
|
||||||
label: t("bodyshop.fields.md_parts_scan.update_value")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Col span={2}>
|
|
||||||
<Space>
|
|
||||||
<DeleteFilled onClick={() => remove(field.name)} />
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() =>
|
|
||||||
add({
|
|
||||||
field: "line_desc",
|
|
||||||
operation: "contains",
|
|
||||||
mark_critical: true,
|
|
||||||
caseInsensitive: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.addpartsrule")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</LayoutFormRow>
|
||||||
</Form.List>
|
)}
|
||||||
</LayoutFormRow>
|
</Form.List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function ShopInfoRbacComponent({ bodyshop }) {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="shop:rbac">
|
<RbacWrapper action="shop:rbac">
|
||||||
<LayoutFormRow>
|
<LayoutFormRow header={t("bodyshop.labels.rbac_options")}>
|
||||||
{[
|
{[
|
||||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||||
? [
|
? [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { Collapse, Divider, Form, Input, InputNumber, Space, Switch } from "antd";
|
import { Col, Collapse, Form, Input, InputNumber, Row, Switch } from "antd";
|
||||||
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";
|
||||||
@@ -6,6 +6,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||||
|
import "./shop-info.responsibilitycenters.taxes.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -16,53 +17,102 @@ const mapDispatchToProps = () => ({
|
|||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibilityCenters);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibilityCenters);
|
||||||
|
|
||||||
|
const taxRootColProps = {
|
||||||
|
xs: 24,
|
||||||
|
sm: 12,
|
||||||
|
md: 8,
|
||||||
|
lg: { flex: "0 0 280px" },
|
||||||
|
xl: { flex: "0 0 240px" },
|
||||||
|
xxl: { flex: "0 0 300px" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const taxTierFieldColProps = {
|
||||||
|
xs: 24,
|
||||||
|
sm: 12,
|
||||||
|
lg: 6
|
||||||
|
};
|
||||||
|
|
||||||
export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
//Iteratively build the form items.
|
const profileTaxCards = [];
|
||||||
const formItems = [];
|
for (let typeNum = 1; typeNum <= 5; typeNum++) {
|
||||||
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
|
const rootTaxItems = getRootTaxFormItems({ typeNum, bodyshop, t });
|
||||||
const section = [];
|
|
||||||
|
|
||||||
section.push(
|
profileTaxCards.push(
|
||||||
TaxFormItems({
|
<LayoutFormRow key={`profile-tax-type-${typeNum}`} header={t("bodyshop.labels.responsibilitycenters.tax_type_card", { typeNum })}>
|
||||||
typeNum: tyCounter,
|
<div style={{ display: "grid", rowGap: 12 }}>
|
||||||
rootElements: true,
|
<Row gutter={[16, 16]} wrap>
|
||||||
bodyshop
|
{rootTaxItems.map((item, index) => (
|
||||||
})
|
<Col key={item.key ?? `tax-root-${typeNum}-${index}`} {...taxRootColProps}>
|
||||||
|
{item}
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
<Row gutter={[12, 12]} wrap className="responsibility-centers-tax-tier-grid">
|
||||||
|
{Array.from({ length: 5 }, (_, index) => {
|
||||||
|
const typeNumIterator = index + 1;
|
||||||
|
const tierTaxItems = getTierTaxFormItems({
|
||||||
|
typeNum,
|
||||||
|
typeNumIterator,
|
||||||
|
t
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
key={`tax-tier-row-${typeNum}-${typeNumIterator}`}
|
||||||
|
xs={24}
|
||||||
|
className="responsibility-centers-tax-tier-grid__col"
|
||||||
|
>
|
||||||
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.responsibilitycenters.tax_tier_card", { typeNumIterator })}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
styles={{
|
||||||
|
header: {
|
||||||
|
paddingInline: 12
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 12
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row gutter={[12, 8]} wrap>
|
||||||
|
{tierTaxItems.map((item, tierIndex) => (
|
||||||
|
<Col key={item.key ?? `tax-tier-${typeNum}-${typeNumIterator}-${tierIndex}`} {...taxTierFieldColProps}>
|
||||||
|
{item}
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
for (let iterator = 1; iterator <= 5; iterator++) {
|
|
||||||
section.push(
|
|
||||||
TaxFormItems({
|
|
||||||
typeNum: tyCounter,
|
|
||||||
typeNumIterator: iterator,
|
|
||||||
rootElements: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
formItems.push(<Space wrap>{section}</Space>);
|
|
||||||
formItems.push(<Divider />);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider titlePlacement="left" orientation="horizontal" style={{ marginTop: ".8rem" }}>
|
<LayoutFormRow header={t("jobs.labels.cieca_pft")}>
|
||||||
{t("jobs.labels.cieca_pft")}
|
<div>{profileTaxCards}</div>
|
||||||
</Divider>
|
</LayoutFormRow>
|
||||||
{formItems}
|
|
||||||
|
|
||||||
<Collapse
|
<LayoutFormRow header={t("bodyshop.labels.responsibilitycenters.default_tax_setup")}>
|
||||||
items={[
|
<Collapse
|
||||||
{
|
items={[
|
||||||
key: "cieca_pfl",
|
{
|
||||||
label: t("jobs.labels.cieca_pfl"),
|
key: "cieca_pfl",
|
||||||
forceRender: true,
|
label: t("jobs.labels.cieca_pfl"),
|
||||||
children: (
|
forceRender: true,
|
||||||
<>
|
children: (
|
||||||
|
<>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -89,7 +139,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +185,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -162,7 +212,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -208,7 +258,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -235,7 +285,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -281,7 +331,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -308,7 +358,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -354,7 +404,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -381,7 +431,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -427,7 +477,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -454,7 +504,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -500,7 +550,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -527,7 +577,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -573,7 +623,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
@@ -673,7 +723,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={2} />
|
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -740,7 +790,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.materials.mat_adjp")}
|
label={t("jobs.fields.materials.mat_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
@@ -767,7 +817,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -825,7 +875,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
label={t("jobs.fields.materials.mat_adjp")}
|
label={t("jobs.fields.materials.mat_adjp")}
|
||||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]}
|
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]}
|
||||||
>
|
>
|
||||||
<InputNumber min={-100} max={100} precision={4} />
|
<InputNumber min={-100} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
@@ -852,7 +902,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -893,15 +943,15 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cieca_pfo",
|
key: "cieca_pfo",
|
||||||
label: t("jobs.labels.cieca_pfo"),
|
label: t("jobs.labels.cieca_pfo"),
|
||||||
forceRender: true,
|
forceRender: true,
|
||||||
children: (
|
children: (
|
||||||
<>
|
<>
|
||||||
<LayoutFormRow noDivider>
|
<LayoutFormRow noDivider>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfo.tow_t_in1")}
|
label={t("jobs.fields.cieca_pfo.tow_t_in1")}
|
||||||
@@ -2145,76 +2195,74 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
|||||||
<InputNumber min={0} max={100} precision={4} />
|
<InputNumber min={0} max={100} precision={4} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
</LayoutFormRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaxFormItems({ typeNum, typeNumIterator, rootElements, bodyshop }) {
|
function getRootTaxFormItems({ typeNum, bodyshop, t }) {
|
||||||
const { t } = useTranslation();
|
return [
|
||||||
|
<Form.Item
|
||||||
if (rootElements)
|
key={`tax_type_${typeNum}_type`}
|
||||||
return (
|
label={t("bodyshop.fields.responsibilitycenter_tax_type", { typeNum })}
|
||||||
<>
|
rules={[
|
||||||
<Form.Item
|
{
|
||||||
label={t("bodyshop.fields.responsibilitycenter_tax_type", {
|
required: true
|
||||||
typeNum,
|
//message: t("general.validation.required"),
|
||||||
typeNumIterator
|
}
|
||||||
})}
|
]}
|
||||||
rules={[
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `tax_type${typeNum}`]}
|
||||||
{
|
>
|
||||||
required: true
|
<Input />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>,
|
||||||
}
|
<Form.Item
|
||||||
]}
|
key={`tax_type_${typeNum}_name`}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `tax_type${typeNum}`]}
|
label={t("bodyshop.fields.responsibilitycenters.state_tax")}
|
||||||
>
|
rules={[
|
||||||
<Input />
|
{
|
||||||
</Form.Item>
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
<Form.Item
|
}
|
||||||
label={t("bodyshop.fields.responsibilitycenters.state_tax")}
|
]}
|
||||||
rules={[
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "name"]}
|
||||||
{
|
>
|
||||||
required: true
|
<Input />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>,
|
||||||
}
|
<Form.Item
|
||||||
]}
|
key={`tax_type_${typeNum}_accountdesc`}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "name"]}
|
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
||||||
>
|
rules={[
|
||||||
<Input />
|
{
|
||||||
</Form.Item>
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
<Form.Item
|
}
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
|
]}
|
||||||
rules={[
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "accountdesc"]}
|
||||||
{
|
>
|
||||||
required: true
|
<Input />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>,
|
||||||
}
|
<Form.Item
|
||||||
]}
|
key={`tax_type_${typeNum}_accountitem`}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "accountdesc"]}
|
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
||||||
>
|
rules={[
|
||||||
<Input />
|
{
|
||||||
</Form.Item>
|
required: true
|
||||||
<Form.Item
|
//message: t("general.validation.required"),
|
||||||
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
}
|
||||||
rules={[
|
]}
|
||||||
{
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "accountitem"]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<Input />
|
||||||
}
|
</Form.Item>,
|
||||||
]}
|
...(bodyshopHasDmsKey(bodyshop)
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, "accountitem"]}
|
? [
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
{bodyshopHasDmsKey(bodyshop) && (
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
key={`tax_type_${typeNum}_dms_acctnumber`}
|
||||||
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
@@ -2226,71 +2274,64 @@ function TaxFormItems({ typeNum, typeNumIterator, rootElements, bodyshop }) {
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
]
|
||||||
</>
|
: [])
|
||||||
);
|
];
|
||||||
return (
|
}
|
||||||
<>
|
|
||||||
<Form.Item
|
function getTierTaxFormItems({ typeNum, typeNumIterator, t }) {
|
||||||
label={t("bodyshop.fields.responsibilitycenter_tax_tier", {
|
return [
|
||||||
typeNum,
|
<Form.Item
|
||||||
typeNumIterator
|
key={`tax_type_${typeNum}_tier_${typeNumIterator}`}
|
||||||
})}
|
label={t("bodyshop.labels.responsibilitycenters.tax_tier_short")}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_tier${typeNumIterator}`]}
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_tier${typeNumIterator}`]}
|
||||||
>
|
>
|
||||||
<InputNumber precision={0} min={0} />
|
<InputNumber precision={0} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>,
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.responsibilitycenter_tax_thres", {
|
key={`tax_type_${typeNum}_threshold_${typeNumIterator}`}
|
||||||
typeNum,
|
label={t("bodyshop.labels.responsibilitycenters.tax_threshold_short")}
|
||||||
typeNumIterator
|
rules={[
|
||||||
})}
|
{
|
||||||
rules={[
|
required: true
|
||||||
{
|
//message: t("general.validation.required"),
|
||||||
required: true
|
}
|
||||||
//message: t("general.validation.required"),
|
]}
|
||||||
}
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_thres${typeNumIterator}`]}
|
||||||
]}
|
>
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_thres${typeNumIterator}`]}
|
<InputNumber min={0} precision={2} />
|
||||||
>
|
</Form.Item>,
|
||||||
<InputNumber min={0} precision={2} />
|
<Form.Item
|
||||||
</Form.Item>
|
key={`tax_type_${typeNum}_rate_${typeNumIterator}`}
|
||||||
<Form.Item
|
label={t("bodyshop.labels.responsibilitycenters.tax_rate_short")}
|
||||||
label={t("bodyshop.fields.responsibilitycenter_tax_rate", {
|
rules={[
|
||||||
typeNum,
|
{
|
||||||
typeNumIterator
|
required: true
|
||||||
})}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_rate${typeNumIterator}`]}
|
||||||
//message: t("general.validation.required"),
|
>
|
||||||
}
|
<InputNumber min={0} precision={2} suffix="%" />
|
||||||
]}
|
</Form.Item>,
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_rate${typeNumIterator}`]}
|
<Form.Item
|
||||||
>
|
key={`tax_type_${typeNum}_surcharge_${typeNumIterator}`}
|
||||||
<InputNumber min={0} precision={2} />
|
label={t("bodyshop.labels.responsibilitycenters.tax_surcharge_short")}
|
||||||
</Form.Item>
|
rules={[
|
||||||
<Form.Item
|
{
|
||||||
label={t("bodyshop.fields.responsibilitycenter_tax_sur", {
|
required: true
|
||||||
typeNum,
|
//message: t("general.validation.required"),
|
||||||
typeNumIterator
|
}
|
||||||
})}
|
]}
|
||||||
rules={[
|
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_sur${typeNumIterator}`]}
|
||||||
{
|
>
|
||||||
required: true
|
<InputNumber min={0} precision={2} suffix="%" />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>
|
||||||
}
|
];
|
||||||
]}
|
|
||||||
name={["md_responsibility_centers", "taxes", `tax_ty${typeNum}`, `ty${typeNum}_sur${typeNumIterator}`]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.responsibility-centers-tax-tier-grid__col.ant-col {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.responsibility-centers-tax-tier-grid__col.ant-col {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
.responsibility-centers-tax-tier-grid__col.ant-col {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2400px) {
|
||||||
|
.responsibility-centers-tax-tier-grid__col.ant-col {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export default function ShopInfoRoGuard({ form }) {
|
|||||||
{() => {
|
{() => {
|
||||||
const disabled = !form.getFieldValue(["md_ro_guard", "enabled"]);
|
const disabled = !form.getFieldValue(["md_ro_guard", "enabled"]);
|
||||||
return (
|
return (
|
||||||
<LayoutFormRow noDivider>
|
<LayoutFormRow header={t("bodyshop.labels.md_ro_guard_options")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_ro_guard.totalgppercent_minimum")}
|
label={t("bodyshop.fields.md_ro_guard.totalgppercent_minimum")}
|
||||||
name={["md_ro_guard", "totalgppercent_minimum"]}
|
name={["md_ro_guard", "totalgppercent_minimum"]}
|
||||||
@@ -32,7 +32,7 @@ export default function ShopInfoRoGuard({ form }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} max={100} precision={1} disabled={disabled} />
|
<InputNumber min={0} max={100} precision={1} suffix="%" disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { CloseOutlined, DeleteFilled, HolderOutlined } from "@ant-design/icons";
|
||||||
|
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||||
|
import { arrayMove, rectSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { Button, Form, Select, Space } from "antd";
|
import { Button, Form, Select, Space } from "antd";
|
||||||
import { useState } from "react";
|
|
||||||
import { ChromePicker } from "react-color";
|
import { ChromePicker } from "react-color";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./shop-info.color.utils";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -24,10 +31,341 @@ const SelectorDiv = styled.div`
|
|||||||
.ant-form-item .ant-select {
|
.ant-form-item .ant-select {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.production-status-color-title-select {
|
||||||
|
min-width: 160px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.production-status-color-title-select .ant-select-selector {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding-inline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.production-status-color-title-select .ant-select-selection-item,
|
||||||
|
.production-status-color-title-select .ant-select-selection-placeholder {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-select .ant-select-selector {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-select .ant-select-selection-wrap {
|
||||||
|
gap: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-inline-end: 6px;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 30px;
|
||||||
|
min-width: 132px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
background: var(--ant-color-fill-quaternary);
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 100%;
|
||||||
|
cursor: grab;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .job-statuses-source-tag-handle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
flex: none;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--ant-color-text-tertiary);
|
||||||
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove:hover {
|
||||||
|
background: var(--ant-color-fill-secondary);
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-statuses-source-tag-wrapper--dragging {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const normalizeStatuses = (statuses) => [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))];
|
||||||
|
|
||||||
|
const getTranslatedDragRect = (active, delta) => {
|
||||||
|
const rect = active?.rect?.current?.initial || active?.rect?.current?.translated;
|
||||||
|
|
||||||
|
if (!rect) return null;
|
||||||
|
|
||||||
|
const x = delta?.x || 0;
|
||||||
|
const y = delta?.y || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: rect.left + x,
|
||||||
|
right: rect.right + x,
|
||||||
|
top: rect.top + y,
|
||||||
|
bottom: rect.bottom + y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPointWithinRect = (point, rect) => {
|
||||||
|
if (!point || !rect) return false;
|
||||||
|
|
||||||
|
return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DraggableStatusTag = ({ label, value, closable, onClose }) => {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
|
id: value
|
||||||
|
});
|
||||||
|
const labelText = String(label ?? value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
ref={setNodeRef}
|
||||||
|
className={`job-statuses-source-tag-wrapper ${isDragging ? "job-statuses-source-tag-wrapper--dragging" : ""}`}
|
||||||
|
data-status-tag-value={value}
|
||||||
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="ant-select-selection-item"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.target.closest(".ant-select-selection-item-remove")) {
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.target.closest(".ant-select-selection-item-remove")) {
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
title={labelText}
|
||||||
|
>
|
||||||
|
<span className="job-statuses-source-tag-handle" aria-hidden>
|
||||||
|
<HolderOutlined />
|
||||||
|
</span>
|
||||||
|
<span className="ant-select-selection-item-content">{labelText}</span>
|
||||||
|
{closable ? (
|
||||||
|
<span
|
||||||
|
className="ant-select-selection-item-remove"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose?.(event);
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableStatusesSelect = ({ value, onChange, mode = "tags", options = [] }) => {
|
||||||
|
const statuses = normalizeStatuses(value);
|
||||||
|
const isTagsMode = mode === "tags";
|
||||||
|
const [knownStatuses, setKnownStatuses] = useState(statuses);
|
||||||
|
const selectWrapperRef = useRef(null);
|
||||||
|
const dragRectRef = useRef(null);
|
||||||
|
const tagSensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 6
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStatusesChange = (nextValues) => {
|
||||||
|
const normalizedNextValues = normalizeStatuses(nextValues);
|
||||||
|
if (isTagsMode) {
|
||||||
|
setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...normalizedNextValues]));
|
||||||
|
}
|
||||||
|
onChange?.(normalizedNextValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isTagsMode) {
|
||||||
|
setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...statuses]));
|
||||||
|
}
|
||||||
|
}, [isTagsMode, statuses]);
|
||||||
|
|
||||||
|
const shouldMoveStatusToEnd = (activeId, dragRect) => {
|
||||||
|
const selectRect =
|
||||||
|
selectWrapperRef.current?.querySelector?.(".ant-select-selector")?.getBoundingClientRect?.() ||
|
||||||
|
selectWrapperRef.current?.getBoundingClientRect?.();
|
||||||
|
if (!dragRect || !selectRect) return false;
|
||||||
|
|
||||||
|
const dragLeadingPoint = {
|
||||||
|
x: dragRect.left,
|
||||||
|
y: dragRect.top
|
||||||
|
};
|
||||||
|
const dragTrailingPoint = {
|
||||||
|
x: dragRect.right,
|
||||||
|
y: dragRect.bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isPointWithinRect(dragLeadingPoint, selectRect) && !isPointWithinRect(dragTrailingPoint, selectRect)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailingStatus = statuses.filter((status) => status !== activeId).at(-1);
|
||||||
|
if (!trailingStatus) return false;
|
||||||
|
|
||||||
|
const trailingTagNode = selectWrapperRef.current?.querySelector?.(
|
||||||
|
`.job-statuses-source-tag-wrapper[data-status-tag-value="${CSS.escape(String(trailingStatus))}"]`
|
||||||
|
);
|
||||||
|
const trailingTagRect = trailingTagNode?.getBoundingClientRect?.();
|
||||||
|
|
||||||
|
if (!trailingTagRect) return false;
|
||||||
|
|
||||||
|
const isOnTrailingRow = dragRect.bottom >= trailingTagRect.top && dragRect.top <= trailingTagRect.bottom;
|
||||||
|
if (isOnTrailingRow) {
|
||||||
|
return dragRect.left >= trailingTagRect.right - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dragRect.top >= trailingTagRect.bottom - 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusSortEnd = ({ active, over, delta }) => {
|
||||||
|
const oldIndex = statuses.indexOf(active.id);
|
||||||
|
const dragRect = dragRectRef.current || getTranslatedDragRect(active, delta);
|
||||||
|
dragRectRef.current = null;
|
||||||
|
|
||||||
|
if (oldIndex < 0) return;
|
||||||
|
|
||||||
|
if (!over) {
|
||||||
|
if (oldIndex !== statuses.length - 1 && shouldMoveStatusToEnd(active.id, dragRect)) {
|
||||||
|
onChange?.(arrayMove(statuses, oldIndex, statuses.length - 1));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active.id === over.id) return;
|
||||||
|
|
||||||
|
const newIndex = statuses.indexOf(over.id);
|
||||||
|
|
||||||
|
if (newIndex < 0) return;
|
||||||
|
|
||||||
|
onChange?.(arrayMove(statuses, oldIndex, newIndex));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStatusTag = ({ label, value: tagValue, closable, onClose }) => {
|
||||||
|
return <DraggableStatusTag closable={closable} label={label} onClose={onClose} value={tagValue} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusSelectOptions = isTagsMode
|
||||||
|
? knownStatuses.map((status) => ({
|
||||||
|
value: status,
|
||||||
|
label: status
|
||||||
|
}))
|
||||||
|
: options;
|
||||||
|
|
||||||
|
if (statuses.length === 0) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="job-statuses-source-select"
|
||||||
|
mode={mode}
|
||||||
|
onChange={handleStatusesChange}
|
||||||
|
options={statusSelectOptions}
|
||||||
|
value={statuses}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={selectWrapperRef}>
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragCancel={() => {
|
||||||
|
dragRectRef.current = null;
|
||||||
|
}}
|
||||||
|
onDragEnd={handleStatusSortEnd}
|
||||||
|
onDragMove={({ active, delta }) => {
|
||||||
|
dragRectRef.current = getTranslatedDragRect(active, delta);
|
||||||
|
}}
|
||||||
|
sensors={tagSensors}
|
||||||
|
>
|
||||||
|
<SortableContext items={statuses} strategy={rectSortingStrategy}>
|
||||||
|
<Select
|
||||||
|
className="job-statuses-source-select"
|
||||||
|
mode={mode}
|
||||||
|
onChange={handleStatusesChange}
|
||||||
|
options={statusSelectOptions}
|
||||||
|
tagRender={renderStatusTag}
|
||||||
|
value={statuses}
|
||||||
|
/>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const allStatuses = normalizeStatuses(Form.useWatch(["md_ro_statuses", "statuses"], form));
|
||||||
|
const productionStatuses = Form.useWatch(["md_ro_statuses", "production_statuses"], form) || [];
|
||||||
|
const additionalBoardStatuses = Form.useWatch(["md_ro_statuses", "additional_board_statuses"], form) || [];
|
||||||
|
const productionColors = Form.useWatch(["md_ro_statuses", "production_colors"], form) || [];
|
||||||
|
const statusOptions = allStatuses;
|
||||||
|
const statusSelectOptions = statusOptions.map((item) => ({ value: item, label: item }));
|
||||||
|
const availableProductionStatuses = [...new Set([...productionStatuses, ...additionalBoardStatuses].filter(Boolean))];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Production_List_Status_Colors }
|
treatments: { Production_List_Status_Colors }
|
||||||
@@ -37,117 +375,119 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const [options, setOptions] = useState(form.getFieldValue(["md_ro_statuses", "statuses"]) || []);
|
|
||||||
|
|
||||||
const [productionStatus, setProductionStatus] = useState(
|
|
||||||
(form.getFieldValue(["md_ro_statuses", "production_statuses"]) || []).concat(
|
|
||||||
form.getFieldValue(["md_ro_statuses", "additional_board_statuses"]) || []
|
|
||||||
) || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleBlur = () => {
|
|
||||||
setOptions(form.getFieldValue(["md_ro_statuses", "statuses"]));
|
|
||||||
setProductionStatus(
|
|
||||||
form
|
|
||||||
.getFieldValue(["md_ro_statuses", "production_statuses"])
|
|
||||||
.concat(form.getFieldValue(["md_ro_statuses", "additional_board_statuses"]))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectorDiv id="jobstatus">
|
<SelectorDiv id="jobstatus">
|
||||||
<Form.Item
|
<LayoutFormRow grow header={t("bodyshop.labels.job_status_options")}>
|
||||||
name={["md_ro_statuses", "statuses"]}
|
<div>
|
||||||
label={t("bodyshop.labels.alljobstatuses")}
|
<Form.Item
|
||||||
rules={[
|
name={["md_ro_statuses", "statuses"]}
|
||||||
{
|
label={t("bodyshop.labels.alljobstatuses")}
|
||||||
required: true,
|
required
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
type: "array"
|
{
|
||||||
}
|
validator: async (_, value) => {
|
||||||
]}
|
const populatedStatuses = normalizeStatuses(value);
|
||||||
>
|
|
||||||
<Select mode="tags" onBlur={handleBlur} />
|
if (populatedStatuses.length === 0) {
|
||||||
</Form.Item>
|
return Promise.reject(
|
||||||
<Form.Item
|
new Error(
|
||||||
name={["md_ro_statuses", "active_statuses"]}
|
t("general.validation.required", {
|
||||||
label={t("bodyshop.fields.statuses.active_statuses")}
|
label: t("bodyshop.labels.alljobstatuses")
|
||||||
rules={[
|
})
|
||||||
{
|
)
|
||||||
required: true,
|
);
|
||||||
//message: t("general.validation.required"),
|
}
|
||||||
type: "array"
|
|
||||||
}
|
if (populatedStatuses.length !== (value || []).filter(Boolean).length) {
|
||||||
]}
|
return Promise.reject(new Error(t("bodyshop.errors.duplicate_job_status")));
|
||||||
>
|
}
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
}
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["md_ro_statuses", "pre_production_statuses"]}
|
>
|
||||||
label={t("bodyshop.fields.statuses.pre_production_statuses")}
|
<SortableStatusesSelect />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
required: true,
|
name={["md_ro_statuses", "active_statuses"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.statuses.active_statuses")}
|
||||||
type: "array"
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true,
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
type: "array"
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["md_ro_statuses", "production_statuses"]}
|
>
|
||||||
label={t("bodyshop.fields.statuses.production_statuses")}
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
required: true,
|
name={["md_ro_statuses", "pre_production_statuses"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.statuses.pre_production_statuses")}
|
||||||
type: "array"
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true,
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
type: "array"
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["md_ro_statuses", "post_production_statuses"]}
|
>
|
||||||
label={t("bodyshop.fields.statuses.post_production_statuses")}
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
required: true,
|
name={["md_ro_statuses", "production_statuses"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.statuses.production_statuses")}
|
||||||
type: "array"
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true,
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
type: "array"
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["md_ro_statuses", "ready_statuses"]}
|
>
|
||||||
label={t("bodyshop.fields.statuses.ready_statuses")}
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
//required: true,
|
name={["md_ro_statuses", "post_production_statuses"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.statuses.post_production_statuses")}
|
||||||
type: "array"
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true,
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
type: "array"
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["md_ro_statuses", "additional_board_statuses"]}
|
>
|
||||||
label={t("bodyshop.fields.statuses.additional_board_statuses")}
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
rules={[
|
</Form.Item>
|
||||||
{
|
<Form.Item
|
||||||
//required: true,
|
name={["md_ro_statuses", "ready_statuses"]}
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.statuses.ready_statuses")}
|
||||||
type: "array"
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
//required: true,
|
||||||
>
|
//message: t("general.validation.required"),
|
||||||
<Select mode="multiple" options={options.map((item) => ({ value: item, label: item }))} />
|
type: "array"
|
||||||
</Form.Item>
|
}
|
||||||
<LayoutFormRow noDivider>
|
]}
|
||||||
|
>
|
||||||
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["md_ro_statuses", "additional_board_statuses"]}
|
||||||
|
label={t("bodyshop.fields.statuses.additional_board_statuses")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
//required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<SortableStatusesSelect mode="multiple" options={statusSelectOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
|
<LayoutFormRow grow header={t("general.actions.defaults")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_scheduled")}
|
label={t("bodyshop.fields.statuses.default_scheduled")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -158,7 +498,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_scheduled"]}
|
name={["md_ro_statuses", "default_scheduled"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_arrived")}
|
label={t("bodyshop.fields.statuses.default_arrived")}
|
||||||
@@ -170,7 +510,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_arrived"]}
|
name={["md_ro_statuses", "default_arrived"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_exported")}
|
label={t("bodyshop.fields.statuses.default_exported")}
|
||||||
@@ -182,7 +522,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_exported"]}
|
name={["md_ro_statuses", "default_exported"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_imported")}
|
label={t("bodyshop.fields.statuses.default_imported")}
|
||||||
@@ -194,7 +534,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_imported"]}
|
name={["md_ro_statuses", "default_imported"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_invoiced")}
|
label={t("bodyshop.fields.statuses.default_invoiced")}
|
||||||
@@ -206,7 +546,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_invoiced"]}
|
name={["md_ro_statuses", "default_invoiced"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_completed")}
|
label={t("bodyshop.fields.statuses.default_completed")}
|
||||||
@@ -218,7 +558,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_completed"]}
|
name={["md_ro_statuses", "default_completed"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_delivered")}
|
label={t("bodyshop.fields.statuses.default_delivered")}
|
||||||
@@ -230,7 +570,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_delivered"]}
|
name={["md_ro_statuses", "default_delivered"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.statuses.default_void")}
|
label={t("bodyshop.fields.statuses.default_void")}
|
||||||
@@ -242,73 +582,122 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
|||||||
]}
|
]}
|
||||||
name={["md_ro_statuses", "default_void"]}
|
name={["md_ro_statuses", "default_void"]}
|
||||||
>
|
>
|
||||||
<Select options={options.map((item) => ({ value: item, label: item }))} />
|
<Select options={statusSelectOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
{Production_List_Status_Colors.treatment === "on" && (
|
{Production_List_Status_Colors.treatment === "on" && (
|
||||||
<LayoutFormRow grow header={t("bodyshop.fields.statuses.production_colors")} id="production_colors">
|
<Form.List name={["md_ro_statuses", "production_colors"]}>
|
||||||
<Form.List name={["md_ro_statuses", "production_colors"]}>
|
{(fields, { add, remove }) => {
|
||||||
{(fields, { add, remove }) => {
|
return (
|
||||||
return (
|
<LayoutFormRow
|
||||||
|
grow
|
||||||
|
header={t("bodyshop.fields.statuses.production_colors")}
|
||||||
|
id="production_colors"
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="add-production-status-color"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
add({
|
||||||
|
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.add_production_status_color")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Space size="large" wrap>
|
{fields.length === 0 ? (
|
||||||
{fields.map((field, index) => (
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_production_status_color")} />
|
||||||
<Form.Item key={field.key}>
|
) : (
|
||||||
<Space orientation="vertical">
|
<Space size="large" wrap align="start">
|
||||||
<div style={{ display: "flex" }}>
|
{fields.map((field, index) => {
|
||||||
<Form.Item
|
const productionColor = productionColors[field.name] || {};
|
||||||
style={{ flex: 1 }}
|
const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color);
|
||||||
label={t("jobs.fields.status")}
|
const selectedProductionColorStatuses = productionColors
|
||||||
key={`${index}status`}
|
.map((item) => item?.status)
|
||||||
name={[field.name, "status"]}
|
.filter(Boolean);
|
||||||
rules={[
|
const productionColorStatusOptions = [
|
||||||
{
|
...new Set([productionColor.status, ...availableProductionStatuses])
|
||||||
required: true
|
]
|
||||||
//message: t("general.validation.required"),
|
.filter(Boolean)
|
||||||
}
|
.filter(
|
||||||
]}
|
(status) =>
|
||||||
>
|
status === productionColor.status || !selectedProductionColorStatuses.includes(status)
|
||||||
<Select options={productionStatus.map((item) => ({ value: item, label: item }))} />
|
);
|
||||||
</Form.Item>
|
|
||||||
<DeleteFilled
|
return (
|
||||||
onClick={() => {
|
<InlineValidatedFormRow
|
||||||
remove(field.name);
|
form={form}
|
||||||
}}
|
errorNames={[["md_ro_statuses", "production_colors", field.name, "status"]]}
|
||||||
/>
|
key={field.key}
|
||||||
</div>
|
noDivider
|
||||||
<Form.Item
|
title={
|
||||||
label={t("bodyshop.fields.statuses.color")}
|
<Form.Item
|
||||||
key={`${index}color`}
|
noStyle
|
||||||
name={[field.name, "color"]}
|
key={`${index}status`}
|
||||||
rules={[
|
name={[field.name, "status"]}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
required: true
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
className="production-status-color-title-select"
|
||||||
|
variant="borderless"
|
||||||
|
placeholder={getFormListItemTitle(
|
||||||
|
t("jobs.fields.status"),
|
||||||
|
index,
|
||||||
|
productionColor.status
|
||||||
|
)}
|
||||||
|
options={productionColorStatusOptions.map((item) => ({
|
||||||
|
value: item,
|
||||||
|
label: item
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...productionColorSurfaceStyles}
|
||||||
|
style={{ width: 260, marginBottom: 0 }}
|
||||||
>
|
>
|
||||||
<ColorPicker />
|
<div>
|
||||||
</Form.Item>
|
<Form.Item
|
||||||
</Space>
|
key={`${index}color`}
|
||||||
</Form.Item>
|
name={[field.name, "color"]}
|
||||||
))}
|
rules={[
|
||||||
</Space>
|
{
|
||||||
<Form.Item>
|
required: true
|
||||||
<Button
|
//message: t("general.validation.required"),
|
||||||
type="dashed"
|
}
|
||||||
onClick={() => {
|
]}
|
||||||
add();
|
>
|
||||||
}}
|
<ColorPicker />
|
||||||
style={{ width: "100%" }}
|
</Form.Item>
|
||||||
>
|
</div>
|
||||||
{t("general.actions.add")}
|
</InlineValidatedFormRow>
|
||||||
</Button>
|
);
|
||||||
</Form.Item>
|
})}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</LayoutFormRow>
|
||||||
}}
|
);
|
||||||
</Form.List>
|
}}
|
||||||
</LayoutFormRow>
|
</Form.List>
|
||||||
)}
|
)}
|
||||||
</SelectorDiv>
|
</SelectorDiv>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch, TimePicker } from "antd";
|
import { Button, Col, Form, Input, InputNumber, Row, Select, Space, Switch, TimePicker, Tooltip } from "antd";
|
||||||
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";
|
||||||
@@ -7,8 +7,16 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
import ColorpickerFormItemComponent from "../form-items-formatted/colorpicker-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 { ColorPicker } from "./shop-info.rostatus.component";
|
import { ColorPicker } from "./shop-info.rostatus.component";
|
||||||
|
import {
|
||||||
|
DEFAULT_TRANSLUCENT_CARD_COLOR,
|
||||||
|
DEFAULT_TRANSLUCENT_PICKER_COLOR,
|
||||||
|
getTintedCardSurfaceStyles
|
||||||
|
} from "./shop-info.color.utils";
|
||||||
|
import "./shop-info.scheduling.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -17,301 +25,514 @@ const mapDispatchToProps = () => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const WORKING_DAYS = [
|
||||||
|
{ key: "sunday", labelKey: "general.labels.sunday" },
|
||||||
|
{ key: "monday", labelKey: "general.labels.monday" },
|
||||||
|
{ key: "tuesday", labelKey: "general.labels.tuesday" },
|
||||||
|
{ key: "wednesday", labelKey: "general.labels.wednesday" },
|
||||||
|
{ key: "thursday", labelKey: "general.labels.thursday" },
|
||||||
|
{ key: "friday", labelKey: "general.labels.friday" },
|
||||||
|
{ key: "saturday", labelKey: "general.labels.saturday" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const APPOINTMENT_COLOR_PICKER_STYLES = {
|
||||||
|
default: {
|
||||||
|
wrap: {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "12px",
|
||||||
|
alignItems: "flex-start"
|
||||||
|
},
|
||||||
|
hue: {
|
||||||
|
flex: "1 1 180px",
|
||||||
|
height: "12px",
|
||||||
|
position: "relative",
|
||||||
|
marginTop: "20px"
|
||||||
|
},
|
||||||
|
swatches: {
|
||||||
|
flex: "1 1 160px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCHEDULING_BUCKET_COLOR_PICKER_STYLES = {
|
||||||
|
default: {
|
||||||
|
picker: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background: "color-mix(in srgb, var(--imex-form-surface) 92%, transparent)",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid color-mix(in srgb, var(--imex-form-surface-border) 72%, transparent)",
|
||||||
|
borderRadius: "8px",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
overflow: "hidden"
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
width: "100%",
|
||||||
|
paddingBottom: "48%",
|
||||||
|
position: "relative",
|
||||||
|
borderRadius: "8px 8px 0 0",
|
||||||
|
overflow: "hidden"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: "12px"
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px"
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
width: "28px"
|
||||||
|
},
|
||||||
|
swatch: {
|
||||||
|
marginTop: "0",
|
||||||
|
width: "12px",
|
||||||
|
height: "12px",
|
||||||
|
borderRadius: "999px"
|
||||||
|
},
|
||||||
|
toggles: {
|
||||||
|
flex: "1"
|
||||||
|
},
|
||||||
|
hue: {
|
||||||
|
height: "10px",
|
||||||
|
position: "relative",
|
||||||
|
marginBottom: "8px"
|
||||||
|
},
|
||||||
|
alpha: {
|
||||||
|
height: "10px",
|
||||||
|
position: "relative"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_TITLE_INPUT_STYLE = {
|
||||||
|
background: "color-mix(in srgb, var(--imex-form-surface) 78%, transparent)",
|
||||||
|
border: "1px solid color-mix(in srgb, var(--imex-form-surface-border) 72%, transparent)",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontWeight: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_TITLE_INPUT_ROW_STYLE = {
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
alignItems: "center",
|
||||||
|
minWidth: 180,
|
||||||
|
maxWidth: "100%"
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_TITLE_INPUT_GROUP_STYLE = {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
minWidth: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const SECTION_TITLE_INPUT_LABEL_STYLE = {
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
opacity: 0.75,
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
};
|
||||||
|
|
||||||
export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
export function ShopInfoSchedulingComponent({ form, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const appointmentColors = Form.useWatch(["appt_colors"], form) || form.getFieldValue(["appt_colors"]) || [];
|
||||||
|
const schedulingBuckets = Form.useWatch(["ssbuckets"], form) || form.getFieldValue(["ssbuckets"]) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LayoutFormRow id="shopinfo-scheduling">
|
<LayoutFormRow grow header={t("bodyshop.labels.scheduling")} id="shopinfo-scheduling">
|
||||||
<Form.Item
|
<>
|
||||||
label={t("bodyshop.fields.appt_length")}
|
<Form.Item
|
||||||
name={"appt_length"}
|
name={["appt_alt_transport"]}
|
||||||
rules={[
|
label={t("bodyshop.fields.appt_alt_transport")}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
}
|
type: "array"
|
||||||
]}
|
}
|
||||||
>
|
]}
|
||||||
<InputNumber min={15} precision={0} />
|
>
|
||||||
</Form.Item>
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.schedule_start_time")}
|
<Form.Item
|
||||||
name={"schedule_start_time"}
|
name={["md_lost_sale_reasons"]}
|
||||||
rules={[
|
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
||||||
{
|
rules={[
|
||||||
required: true
|
{
|
||||||
//message: t("general.validation.required"),
|
// required: true,
|
||||||
}
|
//message: t("general.validation.required"),
|
||||||
]}
|
type: "array"
|
||||||
id="schedule_start_time"
|
}
|
||||||
>
|
]}
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
>
|
||||||
</Form.Item>
|
<Select mode="tags" />
|
||||||
<Form.Item
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.schedule_end_time")}
|
<Row gutter={[16, 0]} wrap>
|
||||||
name={"schedule_end_time"}
|
<Col xs={24} sm={12} xl={6}>
|
||||||
rules={[
|
<Form.Item
|
||||||
{
|
label={t("bodyshop.fields.appt_length")}
|
||||||
required: true
|
name={"appt_length"}
|
||||||
//message: t("general.validation.required"),
|
rules={[
|
||||||
}
|
{
|
||||||
]}
|
required: true
|
||||||
id="schedule_end_time"
|
//message: t("general.validation.required"),
|
||||||
>
|
}
|
||||||
<TimePicker disableSeconds={true} format="HH:mm" />
|
]}
|
||||||
</Form.Item>
|
>
|
||||||
<Form.Item
|
<InputNumber min={15} precision={0} suffix="min" />
|
||||||
name={["appt_alt_transport"]}
|
</Form.Item>
|
||||||
label={t("bodyshop.fields.appt_alt_transport")}
|
</Col>
|
||||||
rules={[
|
<Col xs={24} sm={12} xl={6}>
|
||||||
{
|
<Form.Item
|
||||||
//message: t("general.validation.required"),
|
label={t("bodyshop.fields.schedule_start_time")}
|
||||||
type: "array"
|
name={"schedule_start_time"}
|
||||||
}
|
rules={[
|
||||||
]}
|
{
|
||||||
>
|
required: true
|
||||||
<Select mode="tags" />
|
//message: t("general.validation.required"),
|
||||||
</Form.Item>
|
}
|
||||||
<Form.Item
|
]}
|
||||||
name={["ss_configuration", "dailyhrslimit"]}
|
id="schedule_start_time"
|
||||||
label={t("bodyshop.fields.ss_configuration.dailyhrslimit")}
|
>
|
||||||
>
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
<InputNumber min={0} />
|
</Form.Item>
|
||||||
</Form.Item>
|
</Col>
|
||||||
<Form.Item
|
<Col xs={24} sm={12} xl={6}>
|
||||||
name={["ss_configuration", "nobusinessdays"]}
|
<Form.Item
|
||||||
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
label={t("bodyshop.fields.schedule_end_time")}
|
||||||
valuePropName="checked"
|
name={"schedule_end_time"}
|
||||||
>
|
rules={[
|
||||||
<Switch />
|
{
|
||||||
</Form.Item>
|
required: true
|
||||||
<Form.Item
|
//message: t("general.validation.required"),
|
||||||
name={["md_lost_sale_reasons"]}
|
}
|
||||||
label={t("bodyshop.fields.md_lost_sale_reasons")}
|
]}
|
||||||
rules={[
|
id="schedule_end_time"
|
||||||
{
|
>
|
||||||
// required: true,
|
<TimePicker disableSeconds={true} format="HH:mm" />
|
||||||
//message: t("general.validation.required"),
|
</Form.Item>
|
||||||
type: "array"
|
</Col>
|
||||||
}
|
<Col xs={24} sm={12} xl={6}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
name={["ss_configuration", "dailyhrslimit"]}
|
||||||
<Select mode="tags" />
|
label={t("bodyshop.fields.ss_configuration.dailyhrslimit")}
|
||||||
</Form.Item>
|
>
|
||||||
|
<InputNumber min={0} suffix="hrs" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} xl={6}>
|
||||||
|
<Form.Item
|
||||||
|
name={["ss_configuration", "nobusinessdays"]}
|
||||||
|
label={t("bodyshop.fields.ss_configuration.nobusinessdays")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider titlePlacement="left">{t("bodyshop.labels.workingdays")}</Divider>
|
<LayoutFormRow header={t("bodyshop.labels.workingdays")} id="workingdays">
|
||||||
<Space wrap size="large" id="workingdays">
|
<Space wrap size="middle">
|
||||||
<Form.Item label={t("general.labels.sunday")} name={["workingdays", "sunday"]} valuePropName="checked">
|
{WORKING_DAYS.map(({ key, labelKey }) => (
|
||||||
<Switch />
|
<Form.Item key={key} label={t(labelKey)} name={["workingdays", key]} valuePropName="checked">
|
||||||
</Form.Item>
|
<Switch />
|
||||||
<Form.Item label={t("general.labels.monday")} name={["workingdays", "monday"]} valuePropName="checked">
|
</Form.Item>
|
||||||
<Switch />
|
))}
|
||||||
</Form.Item>
|
</Space>
|
||||||
<Form.Item label={t("general.labels.tuesday")} name={["workingdays", "tuesday"]} valuePropName="checked">
|
</LayoutFormRow>
|
||||||
<Switch />
|
<Form.List name={["appt_colors"]}>
|
||||||
</Form.Item>
|
{(fields, { add, remove, move }) => {
|
||||||
<Form.Item label={t("general.labels.wednesday")} name={["workingdays", "wednesday"]} valuePropName="checked">
|
return (
|
||||||
<Switch />
|
<LayoutFormRow
|
||||||
</Form.Item>
|
header={t("bodyshop.labels.apptcolors")}
|
||||||
<Form.Item label={t("general.labels.thursday")} name={["workingdays", "thursday"]} valuePropName="checked">
|
id="apptcolors"
|
||||||
<Switch />
|
actions={[
|
||||||
</Form.Item>
|
<Button
|
||||||
<Form.Item label={t("general.labels.friday")} name={["workingdays", "friday"]} valuePropName="checked">
|
key="add-appointment-color"
|
||||||
<Switch />
|
type="primary"
|
||||||
</Form.Item>
|
block
|
||||||
<Form.Item label={t("general.labels.saturday")} name={["workingdays", "saturday"]} valuePropName="checked">
|
onClick={() => {
|
||||||
<Switch />
|
add({
|
||||||
</Form.Item>
|
color: {
|
||||||
</Space>
|
...DEFAULT_TRANSLUCENT_PICKER_COLOR,
|
||||||
<LayoutFormRow header={t("bodyshop.labels.apptcolors")} id="apptcolors">
|
rgb: { ...DEFAULT_TRANSLUCENT_PICKER_COLOR.rgb }
|
||||||
<Form.List name={["appt_colors"]}>
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.addapptcolor")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.addapptcolor")} />
|
||||||
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
const appointmentColor =
|
||||||
|
appointmentColors[field.name] || form.getFieldValue(["appt_colors", field.name]) || {};
|
||||||
|
const appointmentColorSurfaceStyles = getTintedCardSurfaceStyles(appointmentColor.color);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle key={field.key}>
|
||||||
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[["appt_colors", field.name, "label"]]}
|
||||||
|
noDivider
|
||||||
|
title={
|
||||||
|
<div style={{ minWidth: 180, maxWidth: "100%" }}>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
key={`${index}aptcolorlabel`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("bodyshop.fields.appt_colors.label")}
|
||||||
|
style={SECTION_TITLE_INPUT_STYLE}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...appointmentColorSurfaceStyles}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
key={`${index}aptcolorcolor`}
|
||||||
|
name={[field.name, "color"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ColorpickerFormItemComponent styles={APPOINTMENT_COLOR_PICKER_STYLES} />
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
{HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && (
|
||||||
|
<Form.List name={["ssbuckets"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
{fields.map((field, index) => (
|
header={t("bodyshop.labels.ssbuckets")}
|
||||||
<Form.Item key={field.key}>
|
id="ssbuckets"
|
||||||
<LayoutFormRow noDivider>
|
actions={[
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.appt_colors.label")}
|
|
||||||
key={`${index}aptcolorlabel`}
|
|
||||||
name={[field.name, "label"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.appt_colors.color")}
|
|
||||||
key={`${index}aptcolorcolor`}
|
|
||||||
name={[field.name, "color"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ColorpickerFormItemComponent />
|
|
||||||
</Form.Item>
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
key="add-job-size-definition"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add();
|
add({
|
||||||
|
color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR }
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
>
|
||||||
{t("bodyshop.actions.addapptcolor")}
|
{t("bodyshop.actions.addbucket")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
]}
|
||||||
</div>
|
>
|
||||||
|
<div>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.addbucket")} />
|
||||||
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
const schedulingBucket =
|
||||||
|
schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {};
|
||||||
|
const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle key={field.key}>
|
||||||
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["ssbuckets", field.name, "id"],
|
||||||
|
["ssbuckets", field.name, "label"]
|
||||||
|
]}
|
||||||
|
noDivider
|
||||||
|
title={
|
||||||
|
<div style={SECTION_TITLE_INPUT_ROW_STYLE}>
|
||||||
|
<div style={SECTION_TITLE_INPUT_GROUP_STYLE}>
|
||||||
|
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}>{t("bodyshop.fields.ssbuckets.id")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
key={`${index}id`}
|
||||||
|
name={[field.name, "id"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("bodyshop.fields.ssbuckets.id")}
|
||||||
|
style={{
|
||||||
|
...SECTION_TITLE_INPUT_STYLE,
|
||||||
|
width: 72
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...SECTION_TITLE_INPUT_GROUP_STYLE,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={SECTION_TITLE_INPUT_LABEL_STYLE}>
|
||||||
|
{t("bodyshop.fields.ssbuckets.label")}
|
||||||
|
</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
key={`${index}label`}
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("bodyshop.fields.ssbuckets.label")}
|
||||||
|
style={{
|
||||||
|
...SECTION_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip title={t("bodyshop.tooltips.reset-color")}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
form.setFieldValue(["ssbuckets", field.name, "color"]);
|
||||||
|
|
||||||
|
form.setFields([
|
||||||
|
{
|
||||||
|
name: ["ssbuckets", field.name, "color"],
|
||||||
|
touched: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
{...schedulingBucketSurfaceStyles}
|
||||||
|
>
|
||||||
|
<div className="shop-info-scheduling__bucket-card-body">
|
||||||
|
<div className="shop-info-scheduling__bucket-card-fields">
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.ssbuckets.gte")}
|
||||||
|
key={`${index}gte`}
|
||||||
|
name={[field.name, "gte"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix="hrs" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.ssbuckets.lt")}
|
||||||
|
key={`${index}lt`}
|
||||||
|
name={[field.name, "lt"]}
|
||||||
|
>
|
||||||
|
<InputNumber suffix="hrs" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.ssbuckets.target")}
|
||||||
|
key={`${index}target`}
|
||||||
|
name={[field.name, "target"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div className="shop-info-scheduling__bucket-card-color">
|
||||||
|
<Form.Item key={`${index}color`} name={[field.name, "color"]}>
|
||||||
|
<ColorPicker styles={SCHEDULING_BUCKET_COLOR_PICKER_STYLES} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
</LayoutFormRow>
|
|
||||||
{HasFeatureAccess({ featureName: "smartscheduling", bodyshop }) && (
|
|
||||||
<LayoutFormRow header={t("bodyshop.labels.ssbuckets")} id="ssbuckets">
|
|
||||||
<Form.List name={["ssbuckets"]}>
|
|
||||||
{(fields, { add, remove, move }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item key={field.key}>
|
|
||||||
<LayoutFormRow noDivider>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.ssbuckets.id")}
|
|
||||||
key={`${index}id`}
|
|
||||||
name={[field.name, "id"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.ssbuckets.label")}
|
|
||||||
key={`${index}label`}
|
|
||||||
name={[field.name, "label"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.ssbuckets.gte")}
|
|
||||||
key={`${index}gte`}
|
|
||||||
name={[field.name, "gte"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.ssbuckets.lt")}
|
|
||||||
key={`${index}lt`}
|
|
||||||
name={[field.name, "lt"]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.ssbuckets.target")}
|
|
||||||
key={`${index}target`}
|
|
||||||
name={[field.name, "target"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<InputNumber />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Space orientation="horizontal">
|
|
||||||
<Form.Item
|
|
||||||
label={
|
|
||||||
<Space>
|
|
||||||
{t("bodyshop.fields.ssbuckets.color")}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
form.setFieldValue(["ssbuckets", field.name, "color"]);
|
|
||||||
|
|
||||||
form.setFields([
|
|
||||||
{
|
|
||||||
name: ["ssbuckets", field.name, "color"],
|
|
||||||
touched: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
key={`${index}color`}
|
|
||||||
name={[field.name, "color"]}
|
|
||||||
>
|
|
||||||
<ColorPicker />
|
|
||||||
</Form.Item>
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.addbucket")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</LayoutFormRow>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
.shop-info-scheduling__bucket-card-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-fields {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(92px, 1fr));
|
||||||
|
gap: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-fields .ant-form-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-color {
|
||||||
|
flex: 0 0 360px;
|
||||||
|
min-width: 360px;
|
||||||
|
max-width: 360px;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-color .ant-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-color .ant-form-item-control,
|
||||||
|
.shop-info-scheduling__bucket-card-color .ant-form-item-control-input,
|
||||||
|
.shop-info-scheduling__bucket-card-color .ant-form-item-control-input-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1199px) {
|
||||||
|
.shop-info-scheduling__bucket-card-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-fields {
|
||||||
|
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-scheduling__bucket-card-color {
|
||||||
|
flex-basis: auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.shop-info-scheduling__bucket-card-fields {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { Select } from "antd";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import "./shop-info.section-navigator.styles.scss";
|
||||||
|
|
||||||
|
const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active";
|
||||||
|
|
||||||
|
export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const targetMapRef = useRef(new Map());
|
||||||
|
const highlightedTargetRef = useRef(null);
|
||||||
|
const [options, setOptions] = useState([]);
|
||||||
|
const [selectedSection, setSelectedSection] = useState(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tabsContainer = tabsRef.current;
|
||||||
|
if (!tabsContainer) return undefined;
|
||||||
|
|
||||||
|
let animationFrameId = 0;
|
||||||
|
|
||||||
|
const refreshOptions = () => {
|
||||||
|
const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active");
|
||||||
|
if (!activePane) {
|
||||||
|
targetMapRef.current = new Map();
|
||||||
|
setOptions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTargetMap = new Map();
|
||||||
|
const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
|
||||||
|
.filter((card) => {
|
||||||
|
return shouldIncludeCardInNavigator(card, activePane);
|
||||||
|
})
|
||||||
|
.map((card, index) => {
|
||||||
|
const { title, depth, searchLabel } = getCardNavigatorInfo(card, activePane);
|
||||||
|
const value = `${activeTabKey}-shop-info-section-${index}`;
|
||||||
|
|
||||||
|
nextTargetMap.set(value, card);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: renderNavigatorOptionLabel(title, depth),
|
||||||
|
labelText: title,
|
||||||
|
searchLabel,
|
||||||
|
depth,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
targetMapRef.current = nextTargetMap;
|
||||||
|
setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions));
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleRefresh = () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = requestAnimationFrame(refreshOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
scheduleRefresh();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(scheduleRefresh);
|
||||||
|
observer.observe(tabsContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [activeTabKey, tabsRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearHighlightedTarget(highlightedTargetRef);
|
||||||
|
setSelectedSection(undefined);
|
||||||
|
}, [activeTabKey]);
|
||||||
|
|
||||||
|
const handleSectionChange = (value) => {
|
||||||
|
setSelectedSection(value);
|
||||||
|
|
||||||
|
clearHighlightedTarget(highlightedTargetRef);
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const target = targetMapRef.current.get(value);
|
||||||
|
if (target) {
|
||||||
|
target.classList.add(HIGHLIGHT_CLASS);
|
||||||
|
highlightedTargetRef.current = target;
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setSelectedSection(undefined);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shop-info-section-navigator">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
value={selectedSection}
|
||||||
|
placeholder={t("bodyshop.labels.jump_to_section")}
|
||||||
|
options={options}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
disabled={options.length === 0}
|
||||||
|
filterOption={(input, option) => option?.searchLabel?.toLowerCase().includes(input.toLowerCase())}
|
||||||
|
onChange={handleSectionChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnCardTitleNode(card) {
|
||||||
|
const headNode = Array.from(card.children).find((child) => child.classList?.contains("ant-card-head"));
|
||||||
|
return headNode?.querySelector(".ant-card-head-title");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnCardTitle(card) {
|
||||||
|
return getOwnCardTitleNode(card)?.textContent?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestorCards(card, activePane) {
|
||||||
|
const ancestors = [];
|
||||||
|
let currentCard = card.parentElement?.closest(".imex-form-row");
|
||||||
|
|
||||||
|
while (currentCard && activePane.contains(currentCard)) {
|
||||||
|
ancestors.push(currentCard);
|
||||||
|
currentCard = currentCard.parentElement?.closest(".imex-form-row");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ancestors.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardDepth(card, activePane) {
|
||||||
|
return getAncestorCards(card, activePane).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleCard(card) {
|
||||||
|
return card.offsetParent !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNavigatorEligibleSubsection(card) {
|
||||||
|
return (
|
||||||
|
!card.classList.contains("imex-form-row--compact") &&
|
||||||
|
!card.classList.contains("imex-form-row--title-only") &&
|
||||||
|
!card.querySelector(":scope > .ant-card-actions")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIncludeCardInNavigator(card, activePane) {
|
||||||
|
const title = getOwnCardTitle(card);
|
||||||
|
if (!title || !isVisibleCard(card)) return false;
|
||||||
|
|
||||||
|
const depth = getCardDepth(card, activePane);
|
||||||
|
if (depth === 0) return true;
|
||||||
|
if (depth === 1) return isNavigatorEligibleSubsection(card);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCardNavigatorInfo(card, activePane) {
|
||||||
|
const title = getOwnCardTitle(card);
|
||||||
|
const ancestors = getAncestorCards(card, activePane);
|
||||||
|
const depth = ancestors.length;
|
||||||
|
const parentTitle = depth === 1 ? getOwnCardTitle(ancestors[0]) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
depth,
|
||||||
|
searchLabel: parentTitle ? `${parentTitle} ${title}` : title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNavigatorOptionLabel(title, depth) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"shop-info-section-navigator__option",
|
||||||
|
depth > 0 ? "shop-info-section-navigator__option--subsection" : null
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
<span className="shop-info-section-navigator__option-label">{title}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHighlightedTarget(highlightedTargetRef) {
|
||||||
|
if (highlightedTargetRef.current) {
|
||||||
|
highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
|
||||||
|
highlightedTargetRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function areOptionsEqual(currentOptions, nextOptions) {
|
||||||
|
if (currentOptions.length !== nextOptions.length) return false;
|
||||||
|
|
||||||
|
return currentOptions.every((option, index) => {
|
||||||
|
const nextOption = nextOptions[index];
|
||||||
|
return (
|
||||||
|
option.labelText === nextOption.labelText &&
|
||||||
|
option.searchLabel === nextOption.searchLabel &&
|
||||||
|
option.depth === nextOption.depth &&
|
||||||
|
option.value === nextOption.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.shop-info-section-navigator {
|
||||||
|
max-width: 360px;
|
||||||
|
width: min(360px, 100%);
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option--subsection {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option--subsection::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
top: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--ant-colorTextDescription);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-info-section-navigator__option-label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imex-form-row.shop-info-section-navigator__target--active.ant-card {
|
||||||
|
border-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--ant-colorPrimary, #1890ff) 65%,
|
||||||
|
var(--imex-form-surface-border)
|
||||||
|
);
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 24%, transparent);
|
||||||
|
transition: border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 12%, var(--imex-form-surface-head));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,23 @@ import { Button, Form, Input, Select, Space } from "antd";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
export default function ShopInfoSpeedPrint() {
|
export default function ShopInfoSpeedPrint() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
const allTemplates = TemplateList("job");
|
const allTemplates = TemplateList("job");
|
||||||
const TemplateListGenerated = InstanceRenderManager({
|
const TemplateListGenerated = InstanceRenderManager({
|
||||||
imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)),
|
imex: Object.fromEntries(Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)),
|
||||||
@@ -18,80 +30,131 @@ export default function ShopInfoSpeedPrint() {
|
|||||||
<Form.List name={["speedprint"]}>
|
<Form.List name={["speedprint"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
{fields.map((field, index) => (
|
header={t("bodyshop.labels.speedprint_configurations")}
|
||||||
<Form.Item key={field.key} style={{ padding: 0, margin: 2 }}>
|
actions={[
|
||||||
<LayoutFormRow grow>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.speedprint.id")}
|
|
||||||
key={`${index}id`}
|
|
||||||
name={[field.name, "id"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.speedprint.label")}
|
|
||||||
key={`${index}label`}
|
|
||||||
name={[field.name, "label"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name={[field.name, "templates"]}
|
|
||||||
label={t("bodyshop.fields.speedprint.templates")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
type: "array"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
options={Object.keys(TemplateListGenerated).map((key) => ({
|
|
||||||
value: TemplateListGenerated[key].key,
|
|
||||||
label: TemplateListGenerated[key].title
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
key="add-speedprint"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add();
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
>
|
||||||
{t("bodyshop.actions.addspeedprint")}
|
{t("bodyshop.actions.addspeedprint")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
]}
|
||||||
</div>
|
>
|
||||||
|
<div>
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.addspeedprint")} />
|
||||||
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle key={field.key}>
|
||||||
|
<InlineValidatedFormRow
|
||||||
|
form={form}
|
||||||
|
errorNames={[
|
||||||
|
["speedprint", field.name, "id"],
|
||||||
|
["speedprint", field.name, "label"]
|
||||||
|
]}
|
||||||
|
noDivider
|
||||||
|
title={
|
||||||
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.id")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "id"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("bodyshop.fields.speedprint.id")}
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("bodyshop.fields.speedprint.label")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "label"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="small"
|
||||||
|
placeholder={t("bodyshop.fields.speedprint.label")}
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.name, "templates"]}
|
||||||
|
label={t("bodyshop.fields.speedprint.templates")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
type: "array"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
options={Object.keys(TemplateListGenerated).map((key) => ({
|
||||||
|
value: TemplateListGenerated[key].key,
|
||||||
|
label: TemplateListGenerated[key].title
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { DeleteFilled } from "@ant-design/icons";
|
|||||||
import { Button, Checkbox, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Button, Checkbox, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -55,10 +57,12 @@ const getTaskPresetAllocationErrors = (presets = [], t) => {
|
|||||||
|
|
||||||
export function ShopInfoTaskPresets({ bodyshop }) {
|
export function ShopInfoTaskPresets({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
const taskPresets = Form.useWatch(["md_tasks_presets", "presets"], form) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LayoutFormRow noDivider>
|
<LayoutFormRow header={t("bodyshop.labels.task_preset_options")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.md_tasks_presets.enable_tasks")}
|
label={t("bodyshop.fields.md_tasks_presets.enable_tasks")}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
@@ -75,187 +79,216 @@ export function ShopInfoTaskPresets({ bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("bodyshop.labels.md_tasks_presets")}>
|
<Form.List
|
||||||
<Form.List
|
name={["md_tasks_presets", "presets"]}
|
||||||
name={["md_tasks_presets", "presets"]}
|
rules={[
|
||||||
rules={[
|
{
|
||||||
{
|
validator: async (_, presets) => {
|
||||||
validator: async (_, presets) => {
|
const allocationErrors = getTaskPresetAllocationErrors(presets, t);
|
||||||
const allocationErrors = getTaskPresetAllocationErrors(presets, t);
|
|
||||||
|
|
||||||
if (allocationErrors.length > 0) {
|
if (allocationErrors.length > 0) {
|
||||||
throw new Error(allocationErrors.join(" "));
|
throw new Error(allocationErrors.join(" "));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]}
|
}
|
||||||
>
|
]}
|
||||||
{(fields, { add, remove, move }, { errors }) => {
|
>
|
||||||
return (
|
{(fields, { add, remove, move }, { errors }) => {
|
||||||
|
return (
|
||||||
|
<LayoutFormRow
|
||||||
|
header={t("bodyshop.labels.md_tasks_presets")}
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
key="add-task-preset"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.add_task_preset")}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.length === 0 ? (
|
||||||
<Form.Item key={field.key}>
|
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_task_preset")} />
|
||||||
<LayoutFormRow noDivider>
|
) : (
|
||||||
<Form.Item
|
fields.map((field, index) => {
|
||||||
label={t("bodyshop.fields.md_tasks_presets.name")}
|
const taskPreset = taskPresets[field.name] || {};
|
||||||
key={`${index}name`}
|
|
||||||
name={[field.name, "name"]}
|
return (
|
||||||
rules={[
|
<Form.Item key={field.key}>
|
||||||
{
|
<LayoutFormRow
|
||||||
required: true
|
noDivider
|
||||||
//message: t("general.validation.required"),
|
title={getFormListItemTitle(
|
||||||
|
t("bodyshop.fields.md_tasks_presets.name"),
|
||||||
|
index,
|
||||||
|
taskPreset.name,
|
||||||
|
taskPreset.memo
|
||||||
|
)}
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
]}
|
>
|
||||||
>
|
<Form.Item
|
||||||
<Input />
|
label={t("bodyshop.fields.md_tasks_presets.name")}
|
||||||
|
key={`${index}name`}
|
||||||
|
name={[field.name, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
span={12}
|
||||||
|
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
|
||||||
|
key={`${index}hourstype`}
|
||||||
|
name={[field.name, "hourstype"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Checkbox.Group>
|
||||||
|
<Row>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAA" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAA")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAB" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAB")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAD" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAD")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAE" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAE")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAF" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAF")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAG" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAG")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAM" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAM")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAR" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAR")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAS" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAS")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LAU" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LAU")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LA1" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LA1")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LA2" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LA2")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LA3" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LA3")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Checkbox value="LA4" style={{ lineHeight: "32px" }}>
|
||||||
|
{t("joblines.fields.lbr_types.LA4")}
|
||||||
|
</Checkbox>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_tasks_presets.percent")}
|
||||||
|
key={`${index}percent`}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={[field.name, "percent"]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} suffix="%" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_tasks_presets.memo")}
|
||||||
|
key={`${index}memo`}
|
||||||
|
name={[field.name, "memo"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
|
||||||
|
key={`${index}nextstatus`}
|
||||||
|
name={[field.name, "nextstatus"]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={bodyshop.md_ro_statuses.production_statuses.map((o) => ({
|
||||||
|
value: o,
|
||||||
|
label: o
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
);
|
||||||
span={12}
|
})
|
||||||
label={t("bodyshop.fields.md_tasks_presets.hourstype")}
|
)}
|
||||||
key={`${index}hourstype`}
|
|
||||||
name={[field.name, "hourstype"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Checkbox.Group>
|
|
||||||
<Row>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAA" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAA")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAB" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAB")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAD" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAD")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAE" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAE")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAF" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAF")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAG" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAG")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAM" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAM")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAR" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAR")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAS" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAS")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LAU" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LAU")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LA1" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LA1")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LA2" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LA2")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LA3" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LA3")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Checkbox value="LA4" style={{ lineHeight: "32px" }}>
|
|
||||||
{t("joblines.fields.lbr_types.LA4")}
|
|
||||||
</Checkbox>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_tasks_presets.percent")}
|
|
||||||
key={`${index}percent`}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
name={[field.name, "percent"]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} max={100} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_tasks_presets.memo")}
|
|
||||||
key={`${index}memo`}
|
|
||||||
name={[field.name, "memo"]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.md_tasks_presets.nextstatus")}
|
|
||||||
key={`${index}nextstatus`}
|
|
||||||
name={[field.name, "nextstatus"]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={bodyshop.md_ro_statuses.production_statuses.map((o) => ({
|
|
||||||
value: o,
|
|
||||||
label: o
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Space wrap>
|
|
||||||
<DeleteFilled
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
|
||||||
</Space>
|
|
||||||
</LayoutFormRow>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.ErrorList errors={errors} />
|
<Form.ErrorList errors={errors} />
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.add_task_preset")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</LayoutFormRow>
|
||||||
}}
|
);
|
||||||
</Form.List>
|
}}
|
||||||
</LayoutFormRow>
|
</Form.List>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -17,19 +18,22 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoIntellipay);
|
|||||||
// noinspection JSUnusedLocalSymbols
|
// noinspection JSUnusedLocalSymbols
|
||||||
export function ShopInfoIntellipay({ bodyshop, form }) {
|
export function ShopInfoIntellipay({ bodyshop, form }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const cashDiscountEnabled = Form.useWatch(["intellipay_config", "enable_cash_discount"], form);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item dependencies={[["intellipay_config", "enable_cash_discount"]]}>
|
{cashDiscountEnabled && (
|
||||||
{() => {
|
<div style={{ marginBottom: 12 }}>
|
||||||
const { intellipay_config } = form.getFieldsValue();
|
<Alert title={t("bodyshop.labels.intellipay_cash_discount")} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
if (intellipay_config?.enable_cash_discount)
|
<LayoutFormRow
|
||||||
return <Alert title={t("bodyshop.labels.intellipay_cash_discount")} />;
|
header={InstanceRenderManager({
|
||||||
}}
|
rome: t("bodyshop.labels.romepay"),
|
||||||
</Form.Item>
|
imex: t("bodyshop.labels.imexpay")
|
||||||
|
})}
|
||||||
<LayoutFormRow noDivider>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
|
label={t("bodyshop.fields.intellipay_config.enable_cash_discount")}
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import {
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Skeleton, Space, Switch, Typography } from "antd";
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
Row,
|
|
||||||
Select,
|
|
||||||
Skeleton,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Tag,
|
|
||||||
Typography
|
|
||||||
} from "antd";
|
|
||||||
|
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
import { useEffect, useState } 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 { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -25,9 +11,22 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.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 {
|
||||||
|
INLINE_TITLE_GROUP_STYLE,
|
||||||
|
INLINE_TITLE_HANDLE_STYLE,
|
||||||
|
INLINE_TITLE_INPUT_STYLE,
|
||||||
|
INLINE_TITLE_LABEL_STYLE,
|
||||||
|
INLINE_TITLE_ROW_STYLE,
|
||||||
|
INLINE_TITLE_SEPARATOR_STYLE,
|
||||||
|
INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||||
|
InlineTitleListIcon
|
||||||
|
} from "../layout-form-row/inline-form-row-title.utils.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
INSERT_EMPLOYEE_TEAM,
|
INSERT_EMPLOYEE_TEAM,
|
||||||
@@ -37,11 +36,10 @@ import {
|
|||||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import {
|
import {
|
||||||
LABOR_TYPES,
|
|
||||||
getSplitTotal,
|
getSplitTotal,
|
||||||
hasExactSplitTotal,
|
hasExactSplitTotal,
|
||||||
|
LABOR_TYPES,
|
||||||
normalizeEmployeeTeam,
|
normalizeEmployeeTeam,
|
||||||
normalizeTeamMember,
|
|
||||||
validateEmployeeTeamMembers
|
validateEmployeeTeamMembers
|
||||||
} from "./shop-employee-teams.form.utils.js";
|
} from "./shop-employee-teams.form.utils.js";
|
||||||
|
|
||||||
@@ -55,24 +53,8 @@ const PAYOUT_METHOD_OPTIONS = [
|
|||||||
{ labelKey: "employee_teams.options.commission_percentage", value: "commission" }
|
{ 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 TEAM_MEMBER_RATE_FIELD_COLS = { xs: 24, sm: 12, md: 8, lg: 6, xxl: 4 };
|
||||||
|
|
||||||
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) => {
|
const formatAllocationPercentage = (percentage) => {
|
||||||
if (percentage === null || percentage === undefined || percentage === "") return null;
|
if (percentage === null || percentage === undefined || percentage === "") return null;
|
||||||
|
|
||||||
@@ -82,16 +64,19 @@ const formatAllocationPercentage = (percentage) => {
|
|||||||
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
return `${numericValue.toFixed(2).replace(/\.?0+$/, "")}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
export function ShopEmployeeTeamsFormComponent({ bodyshop, form, onDirtyChange, isDirty }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [internalForm] = Form.useForm();
|
||||||
|
const [internalIsDirty, setInternalIsDirty] = useState(false);
|
||||||
|
const teamForm = form ?? internalForm;
|
||||||
|
const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty;
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = querystring.parse(useLocation().search);
|
const search = querystring.parse(useLocation().search);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null);
|
const [hydratedTeamId, setHydratedTeamId] = useState(search.employeeTeamId === "new" ? "new" : null);
|
||||||
const isNewTeam = search.employeeTeamId === "new";
|
const isNewTeam = search.employeeTeamId === "new";
|
||||||
|
|
||||||
const { error, data, loading } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
|
const { error, data, loading, refetch } = useQuery(QUERY_EMPLOYEE_TEAM_BY_ID, {
|
||||||
variables: { id: search.employeeTeamId },
|
variables: { id: search.employeeTeamId },
|
||||||
skip: !search.employeeTeamId || isNewTeam,
|
skip: !search.employeeTeamId || isNewTeam,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
@@ -99,29 +84,68 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
notifyOnNetworkStatusChange: true
|
notifyOnNetworkStatusChange: true
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const currentTeamData = data?.employee_teams_by_pk?.id === search.employeeTeamId ? data.employee_teams_by_pk : null;
|
||||||
if (!search.employeeTeamId) return;
|
|
||||||
|
const updateDirtyState = useCallback(
|
||||||
|
(nextDirtyState) => {
|
||||||
|
setInternalIsDirty(nextDirtyState);
|
||||||
|
onDirtyChange?.(nextDirtyState);
|
||||||
|
},
|
||||||
|
[onDirtyChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearTeamFormMeta = useCallback(() => {
|
||||||
|
const fieldMeta = teamForm.getFieldsError().map(({ name }) => ({
|
||||||
|
name,
|
||||||
|
touched: false,
|
||||||
|
validating: false,
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (fieldMeta.length > 0) {
|
||||||
|
teamForm.setFields(fieldMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDirtyState(false);
|
||||||
|
}, [teamForm, updateDirtyState]);
|
||||||
|
|
||||||
|
const resetTeamFormToCurrentData = useCallback(() => {
|
||||||
|
let hydrationFrameId;
|
||||||
|
|
||||||
|
teamForm.resetFields();
|
||||||
|
|
||||||
if (isNewTeam) {
|
if (isNewTeam) {
|
||||||
form.resetFields();
|
|
||||||
setHydratedTeamId("new");
|
setHydratedTeamId("new");
|
||||||
return;
|
hydrationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
clearTeamFormMeta();
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setHydratedTeamId(null);
|
setHydratedTeamId(null);
|
||||||
}, [form, isNewTeam, search.employeeTeamId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (loading) {
|
||||||
if (!search.employeeTeamId || isNewTeam || loading) return;
|
return undefined;
|
||||||
|
|
||||||
if (data?.employee_teams_by_pk?.id === search.employeeTeamId) {
|
|
||||||
form.setFieldsValue(normalizeEmployeeTeam(data.employee_teams_by_pk));
|
|
||||||
setHydratedTeamId(search.employeeTeamId);
|
|
||||||
} else {
|
|
||||||
form.resetFields();
|
|
||||||
setHydratedTeamId(search.employeeTeamId);
|
|
||||||
}
|
}
|
||||||
}, [data, form, isNewTeam, loading, search.employeeTeamId]);
|
|
||||||
|
if (currentTeamData) {
|
||||||
|
teamForm.setFieldsValue(normalizeEmployeeTeam(currentTeamData));
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
setHydratedTeamId(search.employeeTeamId);
|
||||||
|
clearTeamFormMeta();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (hydrationFrameId) window.cancelAnimationFrame(hydrationFrameId);
|
||||||
|
};
|
||||||
|
}, [clearTeamFormMeta, currentTeamData, isNewTeam, loading, search.employeeTeamId, teamForm]);
|
||||||
|
|
||||||
|
useEffect(() => resetTeamFormToCurrentData(), [resetTeamFormToCurrentData]);
|
||||||
|
|
||||||
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
const [updateEmployeeTeam] = useMutation(UPDATE_EMPLOYEE_TEAM);
|
||||||
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
const [insertEmployeeTeam] = useMutation(INSERT_EMPLOYEE_TEAM);
|
||||||
@@ -129,34 +153,25 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
label: t(labelKey),
|
label: t(labelKey),
|
||||||
value
|
value
|
||||||
}));
|
}));
|
||||||
const teamName = Form.useWatch("name", form);
|
const teamName = Form.useWatch("name", teamForm);
|
||||||
const teamMembers = Form.useWatch(["employee_team_members"], form) || [];
|
const teamMembers = Form.useWatch(["employee_team_members"], teamForm) || [];
|
||||||
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
|
const isTeamHydrating = !isNewTeam && Boolean(search.employeeTeamId) && hydratedTeamId !== search.employeeTeamId;
|
||||||
const teamCardTitle = isTeamHydrating
|
const isAllocationTotalExact = hasExactSplitTotal(teamMembers);
|
||||||
? t("employee_teams.fields.name")
|
const allocationTotalValue = formatAllocationPercentage(getSplitTotal(teamMembers))?.replace("%", "") || "0";
|
||||||
: teamName?.trim() || t("employee_teams.fields.name");
|
const teamNameDisplay = teamName?.trim() || t("employee_teams.fields.name");
|
||||||
|
const teamCardTitle = isTeamHydrating ? (
|
||||||
const getTeamMemberTitle = (teamMember = {}) => {
|
t("employee_teams.fields.name")
|
||||||
const employeeName =
|
) : (
|
||||||
getEmployeeDisplayName(bodyshop.employees, teamMember.employeeid) || t("employee_teams.fields.employeeid");
|
<span>
|
||||||
const allocation = formatAllocationPercentage(teamMember.percentage);
|
<span>{teamNameDisplay}</span>
|
||||||
const payoutMethod =
|
<span> - </span>
|
||||||
teamMember.payout_method === "commission"
|
<Typography.Text type={isAllocationTotalExact ? undefined : "danger"}>
|
||||||
? t("employee_teams.options.commission")
|
{t("employee_teams.labels.allocation_total", {
|
||||||
: t("employee_teams.options.hourly");
|
total: allocationTotalValue
|
||||||
|
})}
|
||||||
return (
|
</Typography.Text>
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
|
</span>
|
||||||
<Typography.Text strong>{employeeName}</Typography.Text>
|
);
|
||||||
<Tag variant="filled" color="geekblue">
|
|
||||||
{`${t("employee_teams.fields.allocation")}: ${allocation || "--"}`}
|
|
||||||
</Tag>
|
|
||||||
<Tag variant="filled" color={getPayoutMethodTagColor(teamMember.payout_method)}>
|
|
||||||
{payoutMethod}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
const handleFinish = async ({ employee_team_members = [], ...values }) => {
|
||||||
const { normalizedTeamMembers, errorKey } = validateEmployeeTeamMembers(employee_team_members);
|
const { normalizedTeamMembers, errorKey } = validateEmployeeTeamMembers(employee_team_members);
|
||||||
@@ -193,6 +208,8 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
|
updateDirtyState(false);
|
||||||
|
void refetch();
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("employees.successes.save")
|
title: t("employees.successes.save")
|
||||||
});
|
});
|
||||||
@@ -216,6 +233,7 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_TEAMS"]
|
refetchQueries: ["QUERY_TEAMS"]
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
|
updateDirtyState(false);
|
||||||
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
search.employeeTeamId = response.data.insert_employee_teams_one.id;
|
||||||
history({ search: querystring.stringify(search) });
|
history({ search: querystring.stringify(search) });
|
||||||
notification.success({
|
notification.success({
|
||||||
@@ -230,18 +248,66 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={teamCardTitle}
|
title={isTeamHydrating ? undefined : teamCardTitle}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" onClick={() => form.submit()} disabled={isTeamHydrating}>
|
<Button
|
||||||
{t("general.actions.save")}
|
type="primary"
|
||||||
|
onClick={() => teamForm.submit()}
|
||||||
|
disabled={isTeamHydrating || !resolvedIsDirty}
|
||||||
|
style={{ minWidth: 190 }}
|
||||||
|
>
|
||||||
|
{t("employee_teams.actions.save_team")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isTeamHydrating ? (
|
{isTeamHydrating ? (
|
||||||
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
<Skeleton active title={false} paragraph={{ rows: 12 }} />
|
||||||
) : (
|
) : (
|
||||||
<Form onFinish={handleFinish} autoComplete={"off"} layout="vertical" form={form}>
|
<Form
|
||||||
<LayoutFormRow>
|
onFinish={handleFinish}
|
||||||
|
autoComplete={"off"}
|
||||||
|
layout="vertical"
|
||||||
|
form={teamForm}
|
||||||
|
onValuesChange={() => {
|
||||||
|
updateDirtyState(teamForm.isFieldsTouched());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormsFieldChanged form={teamForm} onReset={resetTeamFormToCurrentData} onDirtyChange={updateDirtyState} />
|
||||||
|
<LayoutFormRow
|
||||||
|
title={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_ROW_STYLE,
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: "var(--ant-font-size-lg)",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
marginRight: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("employee_teams.labels.team_options")}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_SWITCH_GROUP_STYLE,
|
||||||
|
marginLeft: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.active")}</div>
|
||||||
|
<Form.Item noStyle name="active" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="name"
|
name="name"
|
||||||
label={t("employee_teams.fields.name")}
|
label={t("employee_teams.fields.name")}
|
||||||
@@ -253,9 +319,6 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("employee_teams.fields.active")} name="active" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("employee_teams.fields.max_load")}
|
label={t("employee_teams.fields.max_load")}
|
||||||
name="max_load"
|
name="max_load"
|
||||||
@@ -265,128 +328,19 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={1} />
|
<InputNumber min={0} precision={1} suffix="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Form.List name={["employee_team_members"]}>
|
<Form.List name={["employee_team_members"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
{fields.map((field, index) => {
|
title={t("employee_teams.labels.members")}
|
||||||
const teamMember = normalizeTeamMember(teamMembers[field.name]);
|
actions={[
|
||||||
|
|
||||||
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>
|
|
||||||
<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
|
<Button
|
||||||
type="dashed"
|
key="add-team-member"
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add({
|
add({
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
@@ -395,26 +349,166 @@ export function ShopEmployeeTeamsFormComponent({ bodyshop }) {
|
|||||||
commission_rates: {}
|
commission_rates: {}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
>
|
||||||
{t("employee_teams.actions.newmember")}
|
{t("employee_teams.actions.newmember")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
]}
|
||||||
<Form.Item noStyle shouldUpdate>
|
>
|
||||||
{() => {
|
<div>
|
||||||
const teamMembers = form.getFieldValue(["employee_team_members"]) || [];
|
{fields.length === 0 ? (
|
||||||
const splitTotal = getSplitTotal(teamMembers);
|
<ConfigListEmptyState actionLabel={t("employee_teams.actions.newmember")} />
|
||||||
|
) : (
|
||||||
|
fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Item noStyle key={field.key}>
|
||||||
|
<Form.Item name={[field.name, "id"]} hidden>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
<InlineValidatedFormRow
|
||||||
|
form={teamForm}
|
||||||
|
errorNames={[
|
||||||
|
["employee_team_members", field.name, "employeeid"],
|
||||||
|
["employee_team_members", field.name, "percentage"],
|
||||||
|
["employee_team_members", field.name, "payout_method"]
|
||||||
|
]}
|
||||||
|
grow
|
||||||
|
title={
|
||||||
|
<div style={INLINE_TITLE_ROW_STYLE}>
|
||||||
|
<InlineTitleListIcon style={INLINE_TITLE_HANDLE_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.employeeid")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "employeeid"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<EmployeeSearchSelectComponent options={bodyshop.employees} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.allocation")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={[field.name, "percentage"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
precision={2}
|
||||||
|
size="small"
|
||||||
|
aria-label={t("employee_teams.fields.allocation")}
|
||||||
|
suffix="%"
|
||||||
|
style={{
|
||||||
|
...INLINE_TITLE_INPUT_STYLE,
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden style={INLINE_TITLE_SEPARATOR_STYLE} />
|
||||||
|
<div style={INLINE_TITLE_GROUP_STYLE}>
|
||||||
|
<div style={INLINE_TITLE_LABEL_STYLE}>{t("employee_teams.fields.payout_method")}</div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
key={`${index}-payout-method`}
|
||||||
|
name={[field.name, "payout_method"]}
|
||||||
|
initialValue="hourly"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
aria-label={t("employee_teams.fields.payout_method")}
|
||||||
|
size="small"
|
||||||
|
options={payoutMethodOptions}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
styles={{
|
||||||
|
selector: INLINE_TITLE_INPUT_STYLE
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
wrapTitle
|
||||||
|
extra={
|
||||||
|
<Space align="center" size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteFilled />}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows
|
||||||
|
move={move}
|
||||||
|
index={index}
|
||||||
|
total={fields.length}
|
||||||
|
orientation="horizontal"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
dependencies={[["employee_team_members", field.name, "payout_method"]]}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const payoutMethod =
|
||||||
|
teamForm.getFieldValue(["employee_team_members", field.name, "payout_method"]) ||
|
||||||
|
"hourly";
|
||||||
|
const fieldName = payoutMethod === "commission" ? "commission_rates" : "labor_rates";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography.Text type={hasExactSplitTotal(teamMembers) ? undefined : "danger"}>
|
<Row gutter={[16, 0]}>
|
||||||
{t("employee_teams.labels.allocation_total", {
|
{LABOR_TYPES.map((laborType) => (
|
||||||
total: splitTotal.toFixed(2)
|
<Col
|
||||||
})}
|
{...TEAM_MEMBER_RATE_FIELD_COLS}
|
||||||
</Typography.Text>
|
key={`${index}-${fieldName}-${laborType}`}
|
||||||
);
|
>
|
||||||
}}
|
<Form.Item
|
||||||
</Form.Item>
|
label={t(`joblines.fields.lbr_types.${laborType}`)}
|
||||||
</div>
|
name={[field.name, fieldName, laborType]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{payoutMethod === "commission" ? (
|
||||||
|
<InputNumber min={0} max={100} precision={2} suffix="%" />
|
||||||
|
) : (
|
||||||
|
<CurrencyInput prefix="$" />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
</InlineValidatedFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ vi.mock("react-i18next", () => ({
|
|||||||
"employee_teams.options.commission": "Commission",
|
"employee_teams.options.commission": "Commission",
|
||||||
"employee_teams.options.commission_percentage": "Commission",
|
"employee_teams.options.commission_percentage": "Commission",
|
||||||
"employee_teams.actions.newmember": "New Team Member",
|
"employee_teams.actions.newmember": "New Team Member",
|
||||||
|
"employee_teams.actions.save_team": "Save Employee Team",
|
||||||
"employee_teams.errors.minimum_one_member": "Add at least one team member.",
|
"employee_teams.errors.minimum_one_member": "Add at least one team member.",
|
||||||
"employee_teams.errors.duplicate_member": "Team members must be unique.",
|
"employee_teams.errors.duplicate_member": "Team members must be unique.",
|
||||||
"employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.",
|
"employee_teams.errors.allocation_total_exact": "Allocation must total exactly 100%.",
|
||||||
|
"general.labels.click_to_begin": `Click ${values.action ?? ""} to begin`,
|
||||||
"general.actions.save": "Save",
|
"general.actions.save": "Save",
|
||||||
"employees.successes.save": "Saved"
|
"employees.successes.save": "Saved"
|
||||||
};
|
};
|
||||||
@@ -66,6 +68,10 @@ vi.mock("../../contexts/Notifications/notificationContext.jsx", () => ({
|
|||||||
useNotification: () => notification
|
useNotification: () => notification
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../form-fields-changed-alert/form-fields-changed-alert.component.jsx", () => ({
|
||||||
|
default: () => null
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../firebase/firebase.utils", () => ({
|
vi.mock("../../firebase/firebase.utils", () => ({
|
||||||
logImEXEvent: vi.fn()
|
logImEXEvent: vi.fn()
|
||||||
}));
|
}));
|
||||||
@@ -101,11 +107,12 @@ vi.mock("../form-items-formatted/currency-form-item.component", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
vi.mock("../layout-form-row/layout-form-row.component", () => ({
|
||||||
default: ({ title, extra, children }) => (
|
default: ({ title, extra, actions, children }) => (
|
||||||
<div>
|
<div>
|
||||||
{title}
|
{title}
|
||||||
{extra}
|
{extra}
|
||||||
{children}
|
{children}
|
||||||
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
@@ -144,7 +151,7 @@ const addBaseTeamMember = ({ employeeId = "emp-1", percentage = 100, rate = 25 }
|
|||||||
fireEvent.change(screen.getByLabelText("Employee"), {
|
fireEvent.change(screen.getByLabelText("Employee"), {
|
||||||
target: { value: employeeId }
|
target: { value: employeeId }
|
||||||
});
|
});
|
||||||
fireEvent.change(screen.getByRole("spinbutton", { name: "Allocation %" }), {
|
fireEvent.change(screen.getByRole("spinbutton", { name: "Allocation" }), {
|
||||||
target: { value: String(percentage) }
|
target: { value: String(percentage) }
|
||||||
});
|
});
|
||||||
fillHourlyRates(rate);
|
fillHourlyRates(rate);
|
||||||
@@ -211,7 +218,7 @@ describe("ShopEmployeeTeamsFormComponent", () => {
|
|||||||
rate: 27.5
|
rate: 27.5
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
fireEvent.click(screen.getByRole("button", { name: "Save Employee Team" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(insertEmployeeTeamMock).toHaveBeenCalledWith({
|
expect(insertEmployeeTeamMock).toHaveBeenCalledWith({
|
||||||
|
|||||||
@@ -2,20 +2,47 @@ import { Button } from "antd";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
|
|
||||||
export default function ShopEmployeeTeamsListComponent({ loading, employee_teams }) {
|
export default function ShopEmployeeTeamsListComponent({
|
||||||
|
loading,
|
||||||
|
employee_teams,
|
||||||
|
onRequestTeamChange,
|
||||||
|
selectedTeamId
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
|
const navigateToTeam = (employeeTeamId) => {
|
||||||
|
if (onRequestTeamChange) {
|
||||||
|
onRequestTeamChange(employeeTeamId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeTeamId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTeamSelection = () => {
|
||||||
|
const { employeeTeamId, ...nextSearch } = search;
|
||||||
|
void employeeTeamId;
|
||||||
|
history({
|
||||||
|
search: queryString.stringify(nextSearch)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
const handleOnRowClick = (record) => {
|
||||||
if (record) {
|
if (record) {
|
||||||
search.employeeTeamId = record.id;
|
navigateToTeam(record.id);
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
} else {
|
} else {
|
||||||
delete search.employeeTeamId;
|
clearTeamSelection();
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -27,43 +54,38 @@ export default function ShopEmployeeTeamsListComponent({ loading, employee_teams
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow
|
||||||
<ResponsiveTable
|
title={t("bodyshop.labels.employee_teams")}
|
||||||
title={() => {
|
actions={[
|
||||||
return (
|
<Button key="new-team" type="primary" block onClick={() => navigateToTeam("new")}>
|
||||||
<Button
|
{t("employee_teams.actions.new")}
|
||||||
type="primary"
|
</Button>
|
||||||
onClick={() => {
|
]}
|
||||||
search.employeeTeamId = "new";
|
>
|
||||||
history({ search: queryString.stringify(search) });
|
{employee_teams.length === 0 ? (
|
||||||
}}
|
<ConfigListEmptyState actionLabel={t("employee_teams.actions.new")} />
|
||||||
>
|
) : (
|
||||||
{t("employee_teams.actions.new")}
|
<ResponsiveTable
|
||||||
</Button>
|
loading={loading}
|
||||||
);
|
pagination={{ placement: "top" }}
|
||||||
}}
|
columns={columns}
|
||||||
loading={loading}
|
mobileColumnKeys={["name"]}
|
||||||
pagination={{ placement: "top" }}
|
rowKey="id"
|
||||||
columns={columns}
|
dataSource={employee_teams}
|
||||||
mobileColumnKeys={["name"]}
|
rowSelection={{
|
||||||
rowKey="id"
|
onSelect: (props) => navigateToTeam(props.id),
|
||||||
dataSource={employee_teams}
|
type: "radio",
|
||||||
rowSelection={{
|
selectedRowKeys: [selectedTeamId || search.employeeTeamId]
|
||||||
onSelect: (props) => {
|
}}
|
||||||
search.employeeTeamId = props.id;
|
onRow={(record) => {
|
||||||
history({ search: queryString.stringify(search) });
|
return {
|
||||||
},
|
onClick: () => {
|
||||||
type: "radio",
|
handleOnRowClick(record);
|
||||||
selectedRowKeys: [search.employeeTeamId]
|
}
|
||||||
}}
|
};
|
||||||
onRow={(record) => {
|
}}
|
||||||
return {
|
/>
|
||||||
onClick: () => {
|
)}
|
||||||
handleOnRowClick(record);
|
</LayoutFormRow>
|
||||||
}
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,70 @@
|
|||||||
|
import { Form } from "antd";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
|
import queryString from "query-string";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
import { QUERY_TEAMS } from "../../graphql/employee_teams.queries";
|
||||||
|
import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
import ShopEmployeeTeamsListComponent from "./shop-employee-teams.list";
|
||||||
import ShopEmployeeTeamsFormComponent from "./shop-employee-teams.form.component";
|
import ShopEmployeeTeamsFormComponent from "./shop-employee-teams.form.component";
|
||||||
import { Col, Row } from "antd";
|
import "./shop-teams.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
function ShopTeamsContainer() {
|
function ShopTeamsContainer() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isTeamFormDirty, setIsTeamFormDirty] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
const { loading, error, data } = useQuery(QUERY_TEAMS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const hasSelectedTeam = Boolean(search.employeeTeamId);
|
||||||
|
const hasDirtyTeamForm = Boolean(search.employeeTeamId) && isTeamFormDirty;
|
||||||
|
const confirmCloseDirtyTeam = useConfirmDirtyFormNavigation(hasDirtyTeamForm);
|
||||||
|
|
||||||
|
const navigateToTeam = (employeeTeamId) => {
|
||||||
|
if (employeeTeamId === search.employeeTeamId) return;
|
||||||
|
if (!confirmCloseDirtyTeam()) return;
|
||||||
|
|
||||||
|
setIsTeamFormDirty(false);
|
||||||
|
navigate({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...search,
|
||||||
|
employeeTeamId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (error) return <AlertComponent title={error.message} type="error" />;
|
if (error) return <AlertComponent title={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<RbacWrapper action="employee_teams:page">
|
||||||
<RbacWrapper action="employee_teams:page">
|
<div
|
||||||
<Row gutter={[16, 16]}>
|
className={["shop-teams-layout", hasSelectedTeam ? "shop-teams-layout--with-detail" : null]
|
||||||
<Col span={6}>
|
.filter(Boolean)
|
||||||
<ShopEmployeeTeamsListComponent employee_teams={data ? data.employee_teams : []} loading={loading} />
|
.join(" ")}
|
||||||
</Col>
|
>
|
||||||
<Col span={18}>
|
<div className="shop-teams-layout__list">
|
||||||
<ShopEmployeeTeamsFormComponent />
|
<ShopEmployeeTeamsListComponent
|
||||||
</Col>
|
employee_teams={data ? data.employee_teams : []}
|
||||||
</Row>
|
loading={loading}
|
||||||
</RbacWrapper>
|
onRequestTeamChange={navigateToTeam}
|
||||||
</div>
|
selectedTeamId={search.employeeTeamId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{hasSelectedTeam ? (
|
||||||
|
<div className="shop-teams-layout__details">
|
||||||
|
<ShopEmployeeTeamsFormComponent form={form} onDirtyChange={setIsTeamFormDirty} isDirty={isTeamFormDirty} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</RbacWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
client/src/components/shop-teams/shop-teams.styles.scss
Normal file
16
client/src/components/shop-teams/shop-teams.styles.scss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.shop-teams-layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-teams-layout__list,
|
||||||
|
.shop-teams-layout__details {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1700px) {
|
||||||
|
.shop-teams-layout--with-detail {
|
||||||
|
grid-template-columns: minmax(420px, 500px) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { QUERY_SHOP_ASSOCIATIONS } from "../../graphql/user.queries";
|
import { QUERY_SHOP_ASSOCIATIONS } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import ShopUsersAuthEdit from "../shop-users-auth-edit/shop-users-auth-edit.component";
|
import ShopUsersAuthEdit from "../shop-users-auth-edit/shop-users-auth-edit.component";
|
||||||
@@ -66,7 +67,7 @@ export function ShopInfoUsersComponent({ bodyshop }) {
|
|||||||
return <AlertComponent type="error" title={JSON.stringify(error)} />;
|
return <AlertComponent type="error" title={JSON.stringify(error)} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<LayoutFormRow title={t("bodyshop.labels.licensing")}>
|
||||||
<ResponsiveTable
|
<ResponsiveTable
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ placement: "top" }}
|
pagination={{ placement: "top" }}
|
||||||
@@ -75,6 +76,6 @@ export function ShopInfoUsersComponent({ bodyshop }) {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data && data.associations}
|
dataSource={data && data.associations}
|
||||||
/>
|
/>
|
||||||
</div>
|
</LayoutFormRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout";
|
|||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Form, Modal, Space } from "antd";
|
import { Button, Form, Modal, Space } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef } 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";
|
||||||
@@ -15,21 +15,27 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import { buildTimeTicketAuditSummary } from "../../utils/auditTrailDetails.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
timeTicketModal: selectTimeTicket,
|
timeTicketModal: selectTimeTicket,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop }) {
|
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
|
|
||||||
|
const lastSubmittedRef = useRef(null);
|
||||||
|
|
||||||
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
|
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
|
||||||
|
|
||||||
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
||||||
@@ -48,47 +54,77 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const employees = EmployeeAutoCompleteData?.employees ?? [];
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
|
lastSubmittedRef.current = values;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid);
|
const isEdit = Boolean(timeTicketModal.context.id);
|
||||||
if (timeTicketModal.context.id) {
|
const emps = employees.filter((employee) => employee.id === values.employeeid);
|
||||||
updateTicket({
|
const mutation = isEdit
|
||||||
variables: {
|
? updateTicket({
|
||||||
timeticketId: timeTicketModal.context.id,
|
variables: {
|
||||||
timeticket: {
|
timeticketId: timeTicketModal.context.id,
|
||||||
...values,
|
timeticket: {
|
||||||
rate: emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(handleMutationSuccess)
|
|
||||||
.catch(handleMutationError);
|
|
||||||
} else {
|
|
||||||
//Get selected employee rate.
|
|
||||||
insertTicket({
|
|
||||||
variables: {
|
|
||||||
timeTicketInput: [
|
|
||||||
{
|
|
||||||
...values,
|
...values,
|
||||||
rate:
|
rate:
|
||||||
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null,
|
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
created_by: timeTicketModal.context.created_by
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
})
|
||||||
})
|
: insertTicket({
|
||||||
.then(handleMutationSuccess)
|
variables: {
|
||||||
.catch(handleMutationError);
|
timeTicketInput: [
|
||||||
}
|
{
|
||||||
|
...values,
|
||||||
|
rate:
|
||||||
|
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
created_by: timeTicketModal.context.created_by
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mutation.then((result) => handleMutationSuccess(result, isEdit)).catch(handleMutationError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMutationSuccess = () => {
|
const handleMutationSuccess = (result, isEdit) => {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("timetickets.successes.created")
|
title: t("timetickets.successes.created")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const savedTicket =
|
||||||
|
result?.data?.update_timetickets?.returning?.[0] ?? result?.data?.insert_timetickets?.returning?.[0] ?? {};
|
||||||
|
const originalTicket = timeTicketModal.context?.timeticket ?? {};
|
||||||
|
const submittedValues = {
|
||||||
|
...(lastSubmittedRef.current ?? {}),
|
||||||
|
date: lastSubmittedRef.current?.date ?? savedTicket.date ?? originalTicket.date ?? null,
|
||||||
|
employeeid: lastSubmittedRef.current?.employeeid ?? savedTicket.employeeid ?? originalTicket.employeeid ?? null,
|
||||||
|
jobid:
|
||||||
|
lastSubmittedRef.current?.jobid ??
|
||||||
|
savedTicket.jobid ??
|
||||||
|
timeTicketModal.context.jobId ??
|
||||||
|
originalTicket.job?.id ??
|
||||||
|
originalTicket.jobid ??
|
||||||
|
null
|
||||||
|
};
|
||||||
|
const auditSummary = buildTimeTicketAuditSummary({
|
||||||
|
originalTicket,
|
||||||
|
submittedValues,
|
||||||
|
employees
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auditSummary.jobid) {
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: auditSummary.jobid,
|
||||||
|
operation: isEdit
|
||||||
|
? AuditTrailMapping.timeticketupdated(auditSummary.employeeName, auditSummary.date, auditSummary.details)
|
||||||
|
: AuditTrailMapping.timeticketcreated(auditSummary.employeeName, auditSummary.date, auditSummary.details),
|
||||||
|
type: isEdit ? "timeticketupdated" : "timeticketcreated"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh parent screens (Job Labor tab, etc.)
|
// Refresh parent screens (Job Labor tab, etc.)
|
||||||
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();
|
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();
|
||||||
|
|
||||||
|
|||||||
@@ -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: "" }
|
||||||
@@ -62,10 +63,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 +111,7 @@ 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"
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
|
|||||||
employee_prep
|
employee_prep
|
||||||
employee_csr
|
employee_csr
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
|
dms_id
|
||||||
joblines_status {
|
joblines_status {
|
||||||
part_type
|
part_type
|
||||||
status
|
status
|
||||||
@@ -269,6 +270,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
|
|||||||
employee_prep
|
employee_prep
|
||||||
employee_csr
|
employee_csr
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
|
dms_id
|
||||||
joblines_status {
|
joblines_status {
|
||||||
part_type
|
part_type
|
||||||
status
|
status
|
||||||
@@ -2671,6 +2673,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
|
|||||||
suspended
|
suspended
|
||||||
job_totals
|
job_totals
|
||||||
date_repairstarted
|
date_repairstarted
|
||||||
|
dms_id
|
||||||
joblines_status {
|
joblines_status {
|
||||||
part_type
|
part_type
|
||||||
status
|
status
|
||||||
|
|||||||
11
client/src/hooks/useConfirmDirtyFormNavigation.jsx
Normal file
11
client/src/hooks/useConfirmDirtyFormNavigation.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function useConfirmDirtyFormNavigation(isDirty) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
if (!isDirty) return true;
|
||||||
|
return window.confirm(t("general.messages.unsavedchangespopup"));
|
||||||
|
}, [isDirty, t]);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||||
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import JobsCreateComponent from "./jobs-create.component";
|
import JobsCreateComponent from "./jobs-create.component";
|
||||||
import JobCreateContext from "./jobs-create.context";
|
import JobCreateContext from "./jobs-create.context";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,10 +23,11 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) {
|
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
@@ -84,6 +86,11 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
|||||||
newJobId: resp.data.insert_jobs.returning[0].id
|
newJobId: resp.data.insert_jobs.returning[0].id
|
||||||
});
|
});
|
||||||
logImEXEvent("manual_job_create_completed", {});
|
logImEXEvent("manual_job_create_completed", {});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: resp.data.insert_jobs.returning[0].id,
|
||||||
|
operation: AuditTrailMapping.jobmanualcreate(),
|
||||||
|
type: "jobmanualcreate"
|
||||||
|
});
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -120,8 +120,9 @@
|
|||||||
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
|
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
|
||||||
"assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.",
|
"assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.",
|
||||||
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
||||||
|
"billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.",
|
||||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
"billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{details}}.",
|
||||||
"failedpayment": "Failed payment attempt.",
|
"failedpayment": "Failed payment attempt.",
|
||||||
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
||||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||||
@@ -136,6 +137,9 @@
|
|||||||
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
|
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
|
||||||
"jobinvoiced": "Job has been invoiced.",
|
"jobinvoiced": "Job has been invoiced.",
|
||||||
"jobioucreated": "IOU Created.",
|
"jobioucreated": "IOU Created.",
|
||||||
|
"joblineupdate": "Job line {{lineDescription}} updated with the following details: {{details}}.",
|
||||||
|
"jobmanualcreate": "Job manually created.",
|
||||||
|
"jobmanuallineinsert": "Job line manually added with the following details: {{details}}.",
|
||||||
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
||||||
"jobnoteadded": "Note added to Job.",
|
"jobnoteadded": "Note added to Job.",
|
||||||
"jobnotedeleted": "Note deleted from Job.",
|
"jobnotedeleted": "Note deleted from Job.",
|
||||||
@@ -151,7 +155,9 @@
|
|||||||
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
||||||
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
||||||
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
||||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}",
|
||||||
|
"timeticketcreated": "Time Ticket for {{employee}} on {{date}} created with the following details: {{details}}.",
|
||||||
|
"timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
@@ -292,7 +298,23 @@
|
|||||||
},
|
},
|
||||||
"bodyshop": {
|
"bodyshop": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"add_adjuster": "Add Adjuster",
|
||||||
|
"add_control_number": "Add Control Number",
|
||||||
|
"add_cost_center": "Add Cost Center",
|
||||||
|
"add_courtesy_car_rate_preset": "Add Courtesy Car Contract Rate Preset",
|
||||||
|
"add_delivery_checklist_item": "Add Delivery Checklist Item",
|
||||||
|
"add_dms_allocation": "Add DMS Allocation",
|
||||||
|
"add_estimator": "Add Estimator",
|
||||||
|
"add_insurance_company": "Add Insurance Company",
|
||||||
|
"add_intake_checklist_item": "Add Intake Checklist Item",
|
||||||
|
"add_jobline_preset": "Add Jobline Preset",
|
||||||
|
"add_messaging_preset": "Add Messaging Preset",
|
||||||
|
"add_note_preset": "Add Note Preset",
|
||||||
|
"add_parts_order_comment": "Add Parts Order Comment",
|
||||||
|
"add_production_status_color": "Add Production Status Color",
|
||||||
|
"add_profit_center": "Add Profit Center",
|
||||||
"add_task_preset": "Add Task Preset",
|
"add_task_preset": "Add Task Preset",
|
||||||
|
"add_to_email_preset": "Add To Email Preset",
|
||||||
"addapptcolor": "Add Appointment Color",
|
"addapptcolor": "Add Appointment Color",
|
||||||
"addbucket": "Add Definition",
|
"addbucket": "Add Definition",
|
||||||
"addpartslocation": "Add Parts Location",
|
"addpartslocation": "Add Parts Location",
|
||||||
@@ -301,11 +323,13 @@
|
|||||||
"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",
|
||||||
"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",
|
||||||
"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}}",
|
||||||
@@ -403,6 +427,35 @@
|
|||||||
"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",
|
||||||
@@ -410,6 +463,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": {
|
||||||
@@ -463,9 +517,13 @@
|
|||||||
"use_approvals": "Use Time Ticket Approval Queue"
|
"use_approvals": "Use Time Ticket Approval Queue"
|
||||||
},
|
},
|
||||||
"messaginglabel": "Messaging Preset Label",
|
"messaginglabel": "Messaging Preset Label",
|
||||||
|
"messaginglabel_short": "Label",
|
||||||
"messagingtext": "Messaging Preset Text",
|
"messagingtext": "Messaging Preset Text",
|
||||||
|
"messagingtext_short": "Text",
|
||||||
"noteslabel": "Note Label",
|
"noteslabel": "Note Label",
|
||||||
|
"noteslabel_short": "Label",
|
||||||
"notestext": "Note Text",
|
"notestext": "Note Text",
|
||||||
|
"notestext_short": "Text",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||||
"invalid_followers": "Invalid selection. Please select valid employees.",
|
"invalid_followers": "Invalid selection. Please select valid employees.",
|
||||||
@@ -599,12 +657,17 @@
|
|||||||
"federal_tax_itc": "Federal Tax Credit",
|
"federal_tax_itc": "Federal Tax Credit",
|
||||||
"gogcode": "GOG Code (BreakOut)",
|
"gogcode": "GOG Code (BreakOut)",
|
||||||
"gst_override": "GST Override Account #",
|
"gst_override": "GST Override Account #",
|
||||||
|
"invoice_federal_tax_rate_short": "Federal Tax Rate",
|
||||||
|
"invoice_local_tax_rate_short": "Local Tax Rate",
|
||||||
|
"invoice_state_tax_rate_short": "State Tax Rate",
|
||||||
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
|
"invoiceexemptcode": "QuickBooks US - Invoice Tax Exempt Code",
|
||||||
|
"invoiceexemptcode_short": "Invoice Tax Exempt Code",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
"item_type_freight": "Freight",
|
"item_type_freight": "Freight",
|
||||||
"item_type_gog": "GOG",
|
"item_type_gog": "GOG",
|
||||||
"item_type_paint": "Paint Materials",
|
"item_type_paint": "Paint Materials",
|
||||||
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
|
"itemexemptcode": "QuickBooks US - Line Item Tax Exempt Code",
|
||||||
|
"itemexemptcode_short": "Line Item Tax Exempt Code",
|
||||||
"la1": "LA1",
|
"la1": "LA1",
|
||||||
"la2": "LA2",
|
"la2": "LA2",
|
||||||
"la3": "LA3",
|
"la3": "LA3",
|
||||||
@@ -721,6 +784,7 @@
|
|||||||
"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": {
|
||||||
@@ -737,24 +801,33 @@
|
|||||||
},
|
},
|
||||||
"emaillater": "Email Later",
|
"emaillater": "Email Later",
|
||||||
"employee_teams": "Employee Teams",
|
"employee_teams": "Employee Teams",
|
||||||
|
"employee_options": "Employee Options",
|
||||||
|
"employee_rates": "Employee Rates",
|
||||||
|
"employee_vacation": "Employee Vacation",
|
||||||
"employees": "Employees",
|
"employees": "Employees",
|
||||||
"estimators": "Estimators",
|
"estimators": "Estimators",
|
||||||
"filehandlers": "Adjusters",
|
"filehandlers": "Adjusters",
|
||||||
"imexpay": "ImEX Pay",
|
"imexpay": "ImEX Pay",
|
||||||
"insurancecos": "Insurance Companies",
|
"insurancecos": "Insurance Companies",
|
||||||
|
"intake_delivery": "Intake / Delivery Options",
|
||||||
"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",
|
||||||
"jobstatuses": "Job Statuses",
|
"jobstatuses": "Job Statuses",
|
||||||
"laborrates": "Labor Rates",
|
"laborrates": "Labor Rates",
|
||||||
"licensing": "Licensing",
|
"licensing": "Licensing",
|
||||||
"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_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",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": "Notifications"
|
"followers": "Notifications"
|
||||||
},
|
},
|
||||||
@@ -768,11 +841,22 @@
|
|||||||
"qbo_departmentid": "QBO Department ID",
|
"qbo_departmentid": "QBO Department ID",
|
||||||
"qbo_usa": "QBO USA Compatibility",
|
"qbo_usa": "QBO USA Compatibility",
|
||||||
"rbac": "Role Based Access Control",
|
"rbac": "Role Based Access Control",
|
||||||
|
"rbac_options": "Role Based Access Control Options",
|
||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "Cost Centers",
|
"costs": "Cost Centers",
|
||||||
|
"default_tax_setup": "Default Tax Setup",
|
||||||
|
"invoices": "Invoices",
|
||||||
"profits": "Profit Centers",
|
"profits": "Profit Centers",
|
||||||
|
"quickbooks_qbd": "QuickBooks / QBD",
|
||||||
|
"quickbooks_us": "QuickBooks US",
|
||||||
"sales_tax_codes": "Sales Tax Codes",
|
"sales_tax_codes": "Sales Tax Codes",
|
||||||
"tax_accounts": "Tax Accounts",
|
"tax_accounts": "Tax Accounts",
|
||||||
|
"tax_rate_short": "Rate",
|
||||||
|
"tax_surcharge_short": "Surcharge",
|
||||||
|
"tax_threshold_short": "Threshold",
|
||||||
|
"tax_tier_card": "Tier {{typeNumIterator}}",
|
||||||
|
"tax_tier_short": "Tier",
|
||||||
|
"tax_type_card": "Tax Type {{typeNum}}",
|
||||||
"title": "Responsibility Centers",
|
"title": "Responsibility Centers",
|
||||||
"ttl_adjustment": "Subtotal Adjustment Account",
|
"ttl_adjustment": "Subtotal Adjustment Account",
|
||||||
"ttl_tax_adjustment": "Tax Adjustment Account"
|
"ttl_tax_adjustment": "Tax Adjustment Account"
|
||||||
@@ -780,6 +864,9 @@
|
|||||||
"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",
|
||||||
@@ -787,6 +874,7 @@
|
|||||||
"shopinfo": "Shop Information",
|
"shopinfo": "Shop Information",
|
||||||
"shoprates": "Shop Rates",
|
"shoprates": "Shop Rates",
|
||||||
"speedprint": "Speed Print Configuration",
|
"speedprint": "Speed Print Configuration",
|
||||||
|
"speedprint_configurations": "Speed Print Configurations",
|
||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
"task-presets": "Task Presets",
|
"task-presets": "Task Presets",
|
||||||
@@ -810,7 +898,8 @@
|
|||||||
"tooltips": {
|
"tooltips": {
|
||||||
"md_parts_scan": {
|
"md_parts_scan": {
|
||||||
"update_value_tooltip": "Some fields require coded values in order to function properly (e.g. labor and part types). Please reach out to support if you have any questions."
|
"update_value_tooltip": "Some fields require coded values in order to function properly (e.g. labor and part types). Please reach out to support if you have any questions."
|
||||||
}
|
},
|
||||||
|
"reset-color": "Reset color"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"centermustexist": "The chosen responsibility center does not exist.",
|
"centermustexist": "The chosen responsibility center does not exist.",
|
||||||
@@ -1133,6 +1222,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.",
|
||||||
@@ -1178,7 +1268,8 @@
|
|||||||
"employee_teams": {
|
"employee_teams": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"new": "New Team",
|
"new": "New Team",
|
||||||
"newmember": "New Team Member"
|
"newmember": "New Team Member",
|
||||||
|
"save_team": "Save Employee Team"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"allocation_total_exact": "Team allocation must total exactly 100%.",
|
"allocation_total_exact": "Team allocation must total exactly 100%.",
|
||||||
@@ -1196,7 +1287,9 @@
|
|||||||
"percentage": "Percent"
|
"percentage": "Percent"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"allocation_total": "Allocation Total: {{total}}%"
|
"allocation_total": "Allocation Total: {{total}}%",
|
||||||
|
"members": "Members",
|
||||||
|
"team_options": "Team Options"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"commission": "Commission",
|
"commission": "Commission",
|
||||||
@@ -1206,9 +1299,11 @@
|
|||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addrate": "Add Rate",
|
||||||
"addvacation": "Add Vacation",
|
"addvacation": "Add Vacation",
|
||||||
"new": "New Employee",
|
"new": "New Employee",
|
||||||
"newrate": "New Rate",
|
"newrate": "New Rate",
|
||||||
|
"save_employee": "Save Employee",
|
||||||
"select": "Select Employee"
|
"select": "Select Employee"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1240,6 +1335,7 @@
|
|||||||
"labels": {
|
"labels": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"employee_number_short": "Employee #",
|
||||||
"endmustbeafterstart": "End date must be after start date.",
|
"endmustbeafterstart": "End date must be after start date.",
|
||||||
"flat_rate": "Flat Rate",
|
"flat_rate": "Flat Rate",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
@@ -1372,6 +1468,7 @@
|
|||||||
"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",
|
||||||
"confirmpassword": "Confirm Password",
|
"confirmpassword": "Confirm Password",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
@@ -1690,6 +1787,7 @@
|
|||||||
},
|
},
|
||||||
"jobs": {
|
"jobs": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addpayer": "Add Payer",
|
||||||
"addDocuments": "Add Job Documents",
|
"addDocuments": "Add Job Documents",
|
||||||
"addNote": "Add Note",
|
"addNote": "Add Note",
|
||||||
"addtopartsqueue": "Add to Parts Queue",
|
"addtopartsqueue": "Add to Parts Queue",
|
||||||
@@ -1917,10 +2015,15 @@
|
|||||||
"employee_refinish": "Refinish",
|
"employee_refinish": "Refinish",
|
||||||
"est_addr1": "Estimator Address",
|
"est_addr1": "Estimator Address",
|
||||||
"est_co_nm": "Estimator Company",
|
"est_co_nm": "Estimator Company",
|
||||||
|
"est_co_nm_short": "Company",
|
||||||
"est_ct_fn": "Estimator First Name",
|
"est_ct_fn": "Estimator First Name",
|
||||||
|
"est_ct_fn_short": "First Name",
|
||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
|
"est_ct_ln_short": "Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
|
"est_ea_short": "Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
|
"est_ph1_short": "Phone #",
|
||||||
"estimate_approved": "Estimate Approved",
|
"estimate_approved": "Estimate Approved",
|
||||||
"estimate_sent_approval": "Estimate Sent for Approval",
|
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
@@ -1933,9 +2036,13 @@
|
|||||||
"ins_co_nm": "Insurance Company Name",
|
"ins_co_nm": "Insurance Company Name",
|
||||||
"ins_co_nm_short": "Ins. Co.",
|
"ins_co_nm_short": "Ins. Co.",
|
||||||
"ins_ct_fn": "Adjuster First Name",
|
"ins_ct_fn": "Adjuster First Name",
|
||||||
|
"ins_ct_fn_short": "First Name",
|
||||||
"ins_ct_ln": "Adjuster Last Name",
|
"ins_ct_ln": "Adjuster Last Name",
|
||||||
|
"ins_ct_ln_short": "Last Name",
|
||||||
"ins_ea": "Adjuster Email",
|
"ins_ea": "Adjuster Email",
|
||||||
|
"ins_ea_short": "Email",
|
||||||
"ins_ph1": "Adjuster Phone #",
|
"ins_ph1": "Adjuster Phone #",
|
||||||
|
"ins_ph1_short": "Phone #",
|
||||||
"intake": {
|
"intake": {
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"max": "Maximum",
|
"max": "Maximum",
|
||||||
@@ -3160,6 +3267,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",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"assignedlinehours": "",
|
"assignedlinehours": "",
|
||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billupdated": "",
|
"billmarkforreexport": "",
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
@@ -292,7 +292,23 @@
|
|||||||
},
|
},
|
||||||
"bodyshop": {
|
"bodyshop": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"add_adjuster": "",
|
||||||
|
"add_control_number": "",
|
||||||
|
"add_cost_center": "",
|
||||||
|
"add_courtesy_car_rate_preset": "",
|
||||||
|
"add_delivery_checklist_item": "",
|
||||||
|
"add_dms_allocation": "",
|
||||||
|
"add_estimator": "",
|
||||||
|
"add_insurance_company": "",
|
||||||
|
"add_intake_checklist_item": "",
|
||||||
|
"add_jobline_preset": "",
|
||||||
|
"add_messaging_preset": "",
|
||||||
|
"add_note_preset": "",
|
||||||
|
"add_parts_order_comment": "",
|
||||||
|
"add_production_status_color": "",
|
||||||
|
"add_profit_center": "",
|
||||||
"add_task_preset": "",
|
"add_task_preset": "",
|
||||||
|
"add_to_email_preset": "",
|
||||||
"addapptcolor": "",
|
"addapptcolor": "",
|
||||||
"addbucket": "",
|
"addbucket": "",
|
||||||
"addpartslocation": "",
|
"addpartslocation": "",
|
||||||
@@ -301,11 +317,13 @@
|
|||||||
"addtemplate": "",
|
"addtemplate": "",
|
||||||
"newlaborrate": "",
|
"newlaborrate": "",
|
||||||
"newsalestaxcode": "",
|
"newsalestaxcode": "",
|
||||||
|
"save_shop_information": "",
|
||||||
"newstatus": "",
|
"newstatus": "",
|
||||||
"testrender": ""
|
"testrender": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
|
"duplicate_job_status": "",
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
"loading": "No se pueden cargar los detalles de la tienda. Por favor llame al soporte técnico.",
|
||||||
"saving": "",
|
"saving": "",
|
||||||
@@ -403,6 +421,35 @@
|
|||||||
"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": "",
|
||||||
@@ -410,6 +457,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": {
|
||||||
@@ -463,9 +511,13 @@
|
|||||||
"use_approvals": ""
|
"use_approvals": ""
|
||||||
},
|
},
|
||||||
"messaginglabel": "",
|
"messaginglabel": "",
|
||||||
|
"messaginglabel_short": "",
|
||||||
"messagingtext": "",
|
"messagingtext": "",
|
||||||
|
"messagingtext_short": "",
|
||||||
"noteslabel": "",
|
"noteslabel": "",
|
||||||
|
"noteslabel_short": "",
|
||||||
"notestext": "",
|
"notestext": "",
|
||||||
|
"notestext_short": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"invalid_followers": "",
|
"invalid_followers": "",
|
||||||
@@ -599,12 +651,17 @@
|
|||||||
"federal_tax_itc": "",
|
"federal_tax_itc": "",
|
||||||
"gogcode": "",
|
"gogcode": "",
|
||||||
"gst_override": "",
|
"gst_override": "",
|
||||||
|
"invoice_federal_tax_rate_short": "",
|
||||||
|
"invoice_local_tax_rate_short": "",
|
||||||
|
"invoice_state_tax_rate_short": "",
|
||||||
"invoiceexemptcode": "",
|
"invoiceexemptcode": "",
|
||||||
|
"invoiceexemptcode_short": "",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
"item_type_freight": "",
|
"item_type_freight": "",
|
||||||
"item_type_gog": "",
|
"item_type_gog": "",
|
||||||
"item_type_paint": "",
|
"item_type_paint": "",
|
||||||
"itemexemptcode": "",
|
"itemexemptcode": "",
|
||||||
|
"itemexemptcode_short": "",
|
||||||
"la1": "",
|
"la1": "",
|
||||||
"la2": "",
|
"la2": "",
|
||||||
"la3": "",
|
"la3": "",
|
||||||
@@ -721,6 +778,7 @@
|
|||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
"defaultprofitsmapping": "",
|
"defaultprofitsmapping": "",
|
||||||
|
"dms_setup": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
@@ -737,24 +795,33 @@
|
|||||||
},
|
},
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
"employee_teams": "",
|
"employee_teams": "",
|
||||||
|
"employee_options": "",
|
||||||
|
"employee_rates": "",
|
||||||
|
"employee_vacation": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
"filehandlers": "",
|
"filehandlers": "",
|
||||||
"imexpay": "",
|
"imexpay": "",
|
||||||
"insurancecos": "",
|
"insurancecos": "",
|
||||||
|
"intake_delivery": "",
|
||||||
"intakechecklist": "",
|
"intakechecklist": "",
|
||||||
"intellipay_cash_discount": "",
|
"intellipay_cash_discount": "",
|
||||||
|
"job_status_options": "",
|
||||||
"jobstatuses": "",
|
"jobstatuses": "",
|
||||||
"laborrates": "",
|
"laborrates": "",
|
||||||
"licensing": "",
|
"licensing": "",
|
||||||
"md_parts_scan": "",
|
"md_parts_scan": "",
|
||||||
"md_ro_guard": "",
|
"md_ro_guard": "",
|
||||||
|
"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": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
@@ -768,11 +835,22 @@
|
|||||||
"qbo_departmentid": "",
|
"qbo_departmentid": "",
|
||||||
"qbo_usa": "",
|
"qbo_usa": "",
|
||||||
"rbac": "",
|
"rbac": "",
|
||||||
|
"rbac_options": "",
|
||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "",
|
"costs": "",
|
||||||
|
"default_tax_setup": "",
|
||||||
|
"invoices": "",
|
||||||
"profits": "",
|
"profits": "",
|
||||||
|
"quickbooks_qbd": "",
|
||||||
|
"quickbooks_us": "",
|
||||||
"sales_tax_codes": "",
|
"sales_tax_codes": "",
|
||||||
"tax_accounts": "",
|
"tax_accounts": "",
|
||||||
|
"tax_rate_short": "",
|
||||||
|
"tax_surcharge_short": "",
|
||||||
|
"tax_threshold_short": "",
|
||||||
|
"tax_tier_card": "",
|
||||||
|
"tax_tier_short": "",
|
||||||
|
"tax_type_card": "",
|
||||||
"title": "",
|
"title": "",
|
||||||
"ttl_adjustment": "",
|
"ttl_adjustment": "",
|
||||||
"ttl_tax_adjustment": ""
|
"ttl_tax_adjustment": ""
|
||||||
@@ -780,6 +858,9 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"autoemail": "",
|
||||||
|
"jobcosting": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
@@ -787,6 +868,7 @@
|
|||||||
"shopinfo": "",
|
"shopinfo": "",
|
||||||
"shoprates": "",
|
"shoprates": "",
|
||||||
"speedprint": "",
|
"speedprint": "",
|
||||||
|
"speedprint_configurations": "",
|
||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
@@ -810,7 +892,8 @@
|
|||||||
"tooltips": {
|
"tooltips": {
|
||||||
"md_parts_scan": {
|
"md_parts_scan": {
|
||||||
"update_value_tooltip": ""
|
"update_value_tooltip": ""
|
||||||
}
|
},
|
||||||
|
"reset-color": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"centermustexist": "",
|
"centermustexist": "",
|
||||||
@@ -1133,6 +1216,7 @@
|
|||||||
"confirmdelete": "",
|
"confirmdelete": "",
|
||||||
"doctype": "",
|
"doctype": "",
|
||||||
"dragtoupload": "",
|
"dragtoupload": "",
|
||||||
|
"greyscale": "Escala de grises",
|
||||||
"newjobid": "",
|
"newjobid": "",
|
||||||
"openinexplorer": "",
|
"openinexplorer": "",
|
||||||
"optimizedimage": "",
|
"optimizedimage": "",
|
||||||
@@ -1178,7 +1262,8 @@
|
|||||||
"employee_teams": {
|
"employee_teams": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"new": "",
|
"new": "",
|
||||||
"newmember": ""
|
"newmember": "",
|
||||||
|
"save_team": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"allocation_total_exact": "",
|
"allocation_total_exact": "",
|
||||||
@@ -1196,7 +1281,9 @@
|
|||||||
"percentage": ""
|
"percentage": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"allocation_total": ""
|
"allocation_total": "",
|
||||||
|
"members": "",
|
||||||
|
"team_options": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"commission": "",
|
"commission": "",
|
||||||
@@ -1206,9 +1293,11 @@
|
|||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addrate": "",
|
||||||
"addvacation": "",
|
"addvacation": "",
|
||||||
"new": "Nuevo empleado",
|
"new": "Nuevo empleado",
|
||||||
"newrate": "",
|
"newrate": "",
|
||||||
|
"save_employee": "",
|
||||||
"select": ""
|
"select": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1240,6 +1329,7 @@
|
|||||||
"labels": {
|
"labels": {
|
||||||
"actions": "",
|
"actions": "",
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"employee_number_short": "",
|
||||||
"endmustbeafterstart": "",
|
"endmustbeafterstart": "",
|
||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"inactive": "",
|
"inactive": "",
|
||||||
@@ -1372,6 +1462,7 @@
|
|||||||
"beta": "",
|
"beta": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"changelog": "",
|
"changelog": "",
|
||||||
|
"click_to_begin": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1690,6 +1781,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
@@ -1917,10 +2009,15 @@
|
|||||||
"employee_refinish": "",
|
"employee_refinish": "",
|
||||||
"est_addr1": "Dirección del tasador",
|
"est_addr1": "Dirección del tasador",
|
||||||
"est_co_nm": "Tasador",
|
"est_co_nm": "Tasador",
|
||||||
|
"est_co_nm_short": "",
|
||||||
"est_ct_fn": "Nombre del tasador",
|
"est_ct_fn": "Nombre del tasador",
|
||||||
|
"est_ct_fn_short": "",
|
||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
|
"est_ct_ln_short": "",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
|
"est_ea_short": "",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
|
"est_ph1_short": "",
|
||||||
"estimate_approved": "",
|
"estimate_approved": "",
|
||||||
"estimate_sent_approval": "",
|
"estimate_sent_approval": "",
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
@@ -1933,9 +2030,13 @@
|
|||||||
"ins_co_nm": "Nombre de la compañía de seguros",
|
"ins_co_nm": "Nombre de la compañía de seguros",
|
||||||
"ins_co_nm_short": "",
|
"ins_co_nm_short": "",
|
||||||
"ins_ct_fn": "Nombre del controlador de archivos",
|
"ins_ct_fn": "Nombre del controlador de archivos",
|
||||||
|
"ins_ct_fn_short": "",
|
||||||
"ins_ct_ln": "Apellido del manejador de archivos",
|
"ins_ct_ln": "Apellido del manejador de archivos",
|
||||||
|
"ins_ct_ln_short": "",
|
||||||
"ins_ea": "Correo electrónico del controlador de archivos",
|
"ins_ea": "Correo electrónico del controlador de archivos",
|
||||||
|
"ins_ea_short": "",
|
||||||
"ins_ph1": "File Handler Phone #",
|
"ins_ph1": "File Handler Phone #",
|
||||||
|
"ins_ph1_short": "",
|
||||||
"intake": {
|
"intake": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"max": "",
|
"max": "",
|
||||||
@@ -3160,6 +3261,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": "",
|
||||||
|
|||||||
@@ -120,8 +120,8 @@
|
|||||||
"appointmentinsert": "",
|
"appointmentinsert": "",
|
||||||
"assignedlinehours": "",
|
"assignedlinehours": "",
|
||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
|
"billmarkforreexport": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billupdated": "",
|
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
@@ -292,7 +292,23 @@
|
|||||||
},
|
},
|
||||||
"bodyshop": {
|
"bodyshop": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"add_adjuster": "",
|
||||||
|
"add_control_number": "",
|
||||||
|
"add_cost_center": "",
|
||||||
|
"add_courtesy_car_rate_preset": "",
|
||||||
|
"add_delivery_checklist_item": "",
|
||||||
|
"add_dms_allocation": "",
|
||||||
|
"add_estimator": "",
|
||||||
|
"add_insurance_company": "",
|
||||||
|
"add_intake_checklist_item": "",
|
||||||
|
"add_jobline_preset": "",
|
||||||
|
"add_messaging_preset": "",
|
||||||
|
"add_note_preset": "",
|
||||||
|
"add_parts_order_comment": "",
|
||||||
|
"add_production_status_color": "",
|
||||||
|
"add_profit_center": "",
|
||||||
"add_task_preset": "",
|
"add_task_preset": "",
|
||||||
|
"add_to_email_preset": "",
|
||||||
"addapptcolor": "",
|
"addapptcolor": "",
|
||||||
"addbucket": "",
|
"addbucket": "",
|
||||||
"addpartslocation": "",
|
"addpartslocation": "",
|
||||||
@@ -301,11 +317,13 @@
|
|||||||
"addtemplate": "",
|
"addtemplate": "",
|
||||||
"newlaborrate": "",
|
"newlaborrate": "",
|
||||||
"newsalestaxcode": "",
|
"newsalestaxcode": "",
|
||||||
|
"save_shop_information": "",
|
||||||
"newstatus": "",
|
"newstatus": "",
|
||||||
"testrender": ""
|
"testrender": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"creatingdefaultview": "",
|
"creatingdefaultview": "",
|
||||||
|
"duplicate_job_status": "",
|
||||||
"duplicate_insurance_company": "",
|
"duplicate_insurance_company": "",
|
||||||
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
|
||||||
"saving": "",
|
"saving": "",
|
||||||
@@ -403,6 +421,35 @@
|
|||||||
"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": "",
|
||||||
@@ -410,6 +457,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": {
|
||||||
@@ -463,9 +511,13 @@
|
|||||||
"use_approvals": ""
|
"use_approvals": ""
|
||||||
},
|
},
|
||||||
"messaginglabel": "",
|
"messaginglabel": "",
|
||||||
|
"messaginglabel_short": "",
|
||||||
"messagingtext": "",
|
"messagingtext": "",
|
||||||
|
"messagingtext_short": "",
|
||||||
"noteslabel": "",
|
"noteslabel": "",
|
||||||
|
"noteslabel_short": "",
|
||||||
"notestext": "",
|
"notestext": "",
|
||||||
|
"notestext_short": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"invalid_followers": "",
|
"invalid_followers": "",
|
||||||
@@ -599,12 +651,17 @@
|
|||||||
"federal_tax_itc": "",
|
"federal_tax_itc": "",
|
||||||
"gogcode": "",
|
"gogcode": "",
|
||||||
"gst_override": "",
|
"gst_override": "",
|
||||||
|
"invoice_federal_tax_rate_short": "",
|
||||||
|
"invoice_local_tax_rate_short": "",
|
||||||
|
"invoice_state_tax_rate_short": "",
|
||||||
"invoiceexemptcode": "",
|
"invoiceexemptcode": "",
|
||||||
|
"invoiceexemptcode_short": "",
|
||||||
"item_type": "Item Type",
|
"item_type": "Item Type",
|
||||||
"item_type_freight": "",
|
"item_type_freight": "",
|
||||||
"item_type_gog": "",
|
"item_type_gog": "",
|
||||||
"item_type_paint": "",
|
"item_type_paint": "",
|
||||||
"itemexemptcode": "",
|
"itemexemptcode": "",
|
||||||
|
"itemexemptcode_short": "",
|
||||||
"la1": "",
|
"la1": "",
|
||||||
"la2": "",
|
"la2": "",
|
||||||
"la3": "",
|
"la3": "",
|
||||||
@@ -721,6 +778,7 @@
|
|||||||
"customtemplates": "",
|
"customtemplates": "",
|
||||||
"defaultcostsmapping": "",
|
"defaultcostsmapping": "",
|
||||||
"defaultprofitsmapping": "",
|
"defaultprofitsmapping": "",
|
||||||
|
"dms_setup": "",
|
||||||
"deliverchecklist": "",
|
"deliverchecklist": "",
|
||||||
"dms": {
|
"dms": {
|
||||||
"cdk": {
|
"cdk": {
|
||||||
@@ -737,24 +795,33 @@
|
|||||||
},
|
},
|
||||||
"emaillater": "",
|
"emaillater": "",
|
||||||
"employee_teams": "",
|
"employee_teams": "",
|
||||||
|
"employee_options": "",
|
||||||
|
"employee_rates": "",
|
||||||
|
"employee_vacation": "",
|
||||||
"employees": "",
|
"employees": "",
|
||||||
"estimators": "",
|
"estimators": "",
|
||||||
"filehandlers": "",
|
"filehandlers": "",
|
||||||
"imexpay": "",
|
"imexpay": "",
|
||||||
"insurancecos": "",
|
"insurancecos": "",
|
||||||
|
"intake_delivery": "",
|
||||||
"intakechecklist": "",
|
"intakechecklist": "",
|
||||||
"intellipay_cash_discount": "",
|
"intellipay_cash_discount": "",
|
||||||
|
"job_status_options": "",
|
||||||
"jobstatuses": "",
|
"jobstatuses": "",
|
||||||
"laborrates": "",
|
"laborrates": "",
|
||||||
"licensing": "",
|
"licensing": "",
|
||||||
"md_parts_scan": "",
|
"md_parts_scan": "",
|
||||||
"md_ro_guard": "",
|
"md_ro_guard": "",
|
||||||
|
"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": "",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"followers": ""
|
"followers": ""
|
||||||
},
|
},
|
||||||
@@ -768,11 +835,22 @@
|
|||||||
"qbo_departmentid": "",
|
"qbo_departmentid": "",
|
||||||
"qbo_usa": "",
|
"qbo_usa": "",
|
||||||
"rbac": "",
|
"rbac": "",
|
||||||
|
"rbac_options": "",
|
||||||
"responsibilitycenters": {
|
"responsibilitycenters": {
|
||||||
"costs": "",
|
"costs": "",
|
||||||
|
"default_tax_setup": "",
|
||||||
|
"invoices": "",
|
||||||
"profits": "",
|
"profits": "",
|
||||||
|
"quickbooks_qbd": "",
|
||||||
|
"quickbooks_us": "",
|
||||||
"sales_tax_codes": "",
|
"sales_tax_codes": "",
|
||||||
"tax_accounts": "",
|
"tax_accounts": "",
|
||||||
|
"tax_rate_short": "",
|
||||||
|
"tax_surcharge_short": "",
|
||||||
|
"tax_threshold_short": "",
|
||||||
|
"tax_tier_card": "",
|
||||||
|
"tax_tier_short": "",
|
||||||
|
"tax_type_card": "",
|
||||||
"title": "",
|
"title": "",
|
||||||
"ttl_adjustment": "",
|
"ttl_adjustment": "",
|
||||||
"ttl_tax_adjustment": ""
|
"ttl_tax_adjustment": ""
|
||||||
@@ -780,6 +858,9 @@
|
|||||||
"roguard": {
|
"roguard": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
},
|
||||||
|
"autoemail": "",
|
||||||
|
"jobcosting": "",
|
||||||
|
"localmediaserver": "",
|
||||||
"romepay": "",
|
"romepay": "",
|
||||||
"scheduling": "",
|
"scheduling": "",
|
||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
@@ -787,6 +868,7 @@
|
|||||||
"shopinfo": "",
|
"shopinfo": "",
|
||||||
"shoprates": "",
|
"shoprates": "",
|
||||||
"speedprint": "",
|
"speedprint": "",
|
||||||
|
"speedprint_configurations": "",
|
||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
"task-presets": "",
|
"task-presets": "",
|
||||||
@@ -810,7 +892,8 @@
|
|||||||
"tooltips": {
|
"tooltips": {
|
||||||
"md_parts_scan": {
|
"md_parts_scan": {
|
||||||
"update_value_tooltip": ""
|
"update_value_tooltip": ""
|
||||||
}
|
},
|
||||||
|
"reset-color": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"centermustexist": "",
|
"centermustexist": "",
|
||||||
@@ -1133,6 +1216,7 @@
|
|||||||
"confirmdelete": "",
|
"confirmdelete": "",
|
||||||
"doctype": "",
|
"doctype": "",
|
||||||
"dragtoupload": "",
|
"dragtoupload": "",
|
||||||
|
"greyscale": "Niveaux de gris",
|
||||||
"newjobid": "",
|
"newjobid": "",
|
||||||
"openinexplorer": "",
|
"openinexplorer": "",
|
||||||
"optimizedimage": "",
|
"optimizedimage": "",
|
||||||
@@ -1178,7 +1262,8 @@
|
|||||||
"employee_teams": {
|
"employee_teams": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"new": "",
|
"new": "",
|
||||||
"newmember": ""
|
"newmember": "",
|
||||||
|
"save_team": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"allocation_total_exact": "",
|
"allocation_total_exact": "",
|
||||||
@@ -1196,7 +1281,9 @@
|
|||||||
"percentage": ""
|
"percentage": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"allocation_total": ""
|
"allocation_total": "",
|
||||||
|
"members": "",
|
||||||
|
"team_options": ""
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"commission": "",
|
"commission": "",
|
||||||
@@ -1206,9 +1293,11 @@
|
|||||||
},
|
},
|
||||||
"employees": {
|
"employees": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"addrate": "",
|
||||||
"addvacation": "",
|
"addvacation": "",
|
||||||
"new": "Nouvel employé",
|
"new": "Nouvel employé",
|
||||||
"newrate": "",
|
"newrate": "",
|
||||||
|
"save_employee": "",
|
||||||
"select": ""
|
"select": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1240,6 +1329,7 @@
|
|||||||
"labels": {
|
"labels": {
|
||||||
"actions": "",
|
"actions": "",
|
||||||
"active": "",
|
"active": "",
|
||||||
|
"employee_number_short": "",
|
||||||
"endmustbeafterstart": "",
|
"endmustbeafterstart": "",
|
||||||
"flat_rate": "",
|
"flat_rate": "",
|
||||||
"inactive": "",
|
"inactive": "",
|
||||||
@@ -1372,6 +1462,7 @@
|
|||||||
"beta": "",
|
"beta": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"changelog": "",
|
"changelog": "",
|
||||||
|
"click_to_begin": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"confirmpassword": "",
|
"confirmpassword": "",
|
||||||
"created_at": "",
|
"created_at": "",
|
||||||
@@ -1690,6 +1781,7 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
"addtopartsqueue": "",
|
"addtopartsqueue": "",
|
||||||
@@ -1917,10 +2009,15 @@
|
|||||||
"employee_refinish": "",
|
"employee_refinish": "",
|
||||||
"est_addr1": "Adresse de l'évaluateur",
|
"est_addr1": "Adresse de l'évaluateur",
|
||||||
"est_co_nm": "Expert",
|
"est_co_nm": "Expert",
|
||||||
|
"est_co_nm_short": "",
|
||||||
"est_ct_fn": "Prénom de l'évaluateur",
|
"est_ct_fn": "Prénom de l'évaluateur",
|
||||||
|
"est_ct_fn_short": "",
|
||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
|
"est_ct_ln_short": "",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
|
"est_ea_short": "",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
|
"est_ph1_short": "",
|
||||||
"estimate_approved": "",
|
"estimate_approved": "",
|
||||||
"estimate_sent_approval": "",
|
"estimate_sent_approval": "",
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
@@ -1933,9 +2030,13 @@
|
|||||||
"ins_co_nm": "Nom de la compagnie d'assurance",
|
"ins_co_nm": "Nom de la compagnie d'assurance",
|
||||||
"ins_co_nm_short": "",
|
"ins_co_nm_short": "",
|
||||||
"ins_ct_fn": "Prénom du gestionnaire de fichiers",
|
"ins_ct_fn": "Prénom du gestionnaire de fichiers",
|
||||||
|
"ins_ct_fn_short": "",
|
||||||
"ins_ct_ln": "Nom du gestionnaire de fichiers",
|
"ins_ct_ln": "Nom du gestionnaire de fichiers",
|
||||||
|
"ins_ct_ln_short": "",
|
||||||
"ins_ea": "Courriel du gestionnaire de fichiers",
|
"ins_ea": "Courriel du gestionnaire de fichiers",
|
||||||
|
"ins_ea_short": "",
|
||||||
"ins_ph1": "Numéro de téléphone du gestionnaire de fichiers",
|
"ins_ph1": "Numéro de téléphone du gestionnaire de fichiers",
|
||||||
|
"ins_ph1_short": "",
|
||||||
"intake": {
|
"intake": {
|
||||||
"label": "",
|
"label": "",
|
||||||
"max": "",
|
"max": "",
|
||||||
@@ -3160,6 +3261,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": "",
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const AuditTrailMapping = {
|
|||||||
appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
|
appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
|
||||||
appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }),
|
appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }),
|
||||||
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||||
|
billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }),
|
||||||
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||||
billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }),
|
billupdated: (invoice_number, details) => i18n.t("audit_trail.messages.billupdated", { invoice_number, details }),
|
||||||
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
||||||
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
|
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
|
||||||
jobchecklist: (type, inproduction, status) =>
|
jobchecklist: (type, inproduction, status) =>
|
||||||
@@ -25,6 +26,10 @@ const AuditTrailMapping = {
|
|||||||
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
||||||
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
||||||
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
||||||
|
joblineupdate: (lineDescription, details) =>
|
||||||
|
i18n.t("audit_trail.messages.joblineupdate", { details, lineDescription }),
|
||||||
|
jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"),
|
||||||
|
jobmanuallineinsert: (details) => i18n.t("audit_trail.messages.jobmanuallineinsert", { details }),
|
||||||
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
||||||
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
||||||
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
||||||
@@ -71,7 +76,11 @@ const AuditTrailMapping = {
|
|||||||
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
||||||
title,
|
title,
|
||||||
uncompletedBy
|
uncompletedBy
|
||||||
})
|
}),
|
||||||
|
timeticketcreated: (employee, date, details) =>
|
||||||
|
i18n.t("audit_trail.messages.timeticketcreated", { employee, date, details }),
|
||||||
|
timeticketupdated: (employee, date, details) =>
|
||||||
|
i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details })
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuditTrailMapping;
|
export default AuditTrailMapping;
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
186
client/src/utils/auditTrailDetails.js
Normal file
186
client/src/utils/auditTrailDetails.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import dayjs from "./day";
|
||||||
|
|
||||||
|
const EMPTY_VALUE = "<<empty>>";
|
||||||
|
const NO_CHANGES = "No changes";
|
||||||
|
|
||||||
|
const BILL_LINE_KEYS = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"];
|
||||||
|
const JOB_LINE_SKIP_KEYS = new Set(["ah_detail_line", "prt_dsmk_p"]);
|
||||||
|
const DATE_ONLY_KEYS = new Set(["date"]);
|
||||||
|
const DATE_TIME_KEYS = new Set(["clockon", "clockoff"]);
|
||||||
|
const CURRENCY_KEYS = new Set(["actual_price", "actual_cost", "act_price", "db_price", "rate"]);
|
||||||
|
const HOUR_KEYS = new Set(["productivehrs", "actualhrs", "mod_lb_hrs"]);
|
||||||
|
|
||||||
|
const isBlank = (value) => value == null || value === "";
|
||||||
|
|
||||||
|
const isStructuredValue = (value) => value != null && typeof value === "object" && !dayjs.isDayjs?.(value);
|
||||||
|
|
||||||
|
const formatDate = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD"));
|
||||||
|
|
||||||
|
const formatDateTime = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD HH:mm"));
|
||||||
|
|
||||||
|
const formatNumber = (value, fractionDigits) =>
|
||||||
|
typeof value === "number" ? value.toFixed(fractionDigits) : String(value);
|
||||||
|
|
||||||
|
const compareValue = (key, value) => {
|
||||||
|
if (isBlank(value)) return EMPTY_VALUE;
|
||||||
|
if (DATE_TIME_KEYS.has(key)) return formatDateTime(value);
|
||||||
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
||||||
|
if (dayjs.isDayjs?.(value)) return formatDateTime(value);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFieldChangeDetails = ({ keys, original = {}, updated = {}, displayValue, skippedKeys = new Set() }) =>
|
||||||
|
keys
|
||||||
|
.filter((key) => key !== "__typename" && !skippedKeys.has(key))
|
||||||
|
.filter((key) => !isStructuredValue(original[key]) && !isStructuredValue(updated[key]))
|
||||||
|
.map((key) => {
|
||||||
|
if (compareValue(key, original[key]) === compareValue(key, updated[key])) return null;
|
||||||
|
return `${key}: ${displayValue(key, original[key])} -> ${displayValue(key, updated[key])}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const formatBillValue = (key, value) => {
|
||||||
|
if (isBlank(value)) return EMPTY_VALUE;
|
||||||
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
||||||
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatJobLineValue = (key, value) => {
|
||||||
|
if (isBlank(value)) return EMPTY_VALUE;
|
||||||
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
||||||
|
if (HOUR_KEYS.has(key)) return formatNumber(value, 1);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmployeeName = (employeeId, employees = [], fallbackEmployee) => {
|
||||||
|
if (
|
||||||
|
(employeeId == null || fallbackEmployee?.id === employeeId) &&
|
||||||
|
(fallbackEmployee?.first_name || fallbackEmployee?.last_name)
|
||||||
|
) {
|
||||||
|
return [fallbackEmployee.first_name, fallbackEmployee.last_name].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const employee = employees.find(({ id }) => id === employeeId);
|
||||||
|
if (employee) {
|
||||||
|
return [employee.first_name, employee.last_name].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return employeeId ? String(employeeId) : EMPTY_VALUE;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimeTicketValue = (key, value, { employees = [], fallbackEmployee } = {}) => {
|
||||||
|
if (isBlank(value)) return EMPTY_VALUE;
|
||||||
|
if (key === "employeeid") return getEmployeeName(value, employees, fallbackEmployee);
|
||||||
|
if (DATE_TIME_KEYS.has(key)) return formatDateTime(value);
|
||||||
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
||||||
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
||||||
|
if (HOUR_KEYS.has(key)) return formatNumber(value, 1);
|
||||||
|
if (typeof value === "boolean") return value ? "true" : "false";
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildBillLineSummary = (line) =>
|
||||||
|
BILL_LINE_KEYS.map((key) => `${key}: ${formatBillValue(key, line[key])}`).join(", ");
|
||||||
|
|
||||||
|
export function buildBillUpdateAuditDetails({ originalBill = {}, bill = {}, billlines = [] }) {
|
||||||
|
const updatedBill = { ...bill, billlines };
|
||||||
|
const billKeys = Array.from(new Set([...Object.keys(originalBill), ...Object.keys(updatedBill)])).filter(
|
||||||
|
(key) => key !== "billlines"
|
||||||
|
);
|
||||||
|
|
||||||
|
const changed = buildFieldChangeDetails({
|
||||||
|
keys: billKeys,
|
||||||
|
original: originalBill,
|
||||||
|
updated: updatedBill,
|
||||||
|
displayValue: formatBillValue
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalBillLines = originalBill.billlines ?? [];
|
||||||
|
const updatedBillLines = updatedBill.billlines ?? [];
|
||||||
|
|
||||||
|
const addedLines = updatedBillLines
|
||||||
|
.filter((line) => !line.id)
|
||||||
|
.map((line) => `+${line.line_desc || line.description || "new line"} (${buildBillLineSummary(line)})`);
|
||||||
|
|
||||||
|
const removedLines = originalBillLines
|
||||||
|
.filter((line) => !updatedBillLines.some((updatedLine) => updatedLine.id === line.id))
|
||||||
|
.map(
|
||||||
|
(line) => `-${line.line_desc || line.description || line.id || "removed line"} (${buildBillLineSummary(line)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const modifiedLines = updatedBillLines
|
||||||
|
.filter((line) => line.id)
|
||||||
|
.flatMap((line) => {
|
||||||
|
const originalLine = originalBillLines.find(({ id }) => id === line.id);
|
||||||
|
if (!originalLine) return [];
|
||||||
|
|
||||||
|
const lineChanges = buildFieldChangeDetails({
|
||||||
|
keys: BILL_LINE_KEYS,
|
||||||
|
original: originalLine,
|
||||||
|
updated: line,
|
||||||
|
displayValue: formatBillValue
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lineChanges.length) return [];
|
||||||
|
|
||||||
|
return [`${line.line_desc || line.description || line.id}: ${lineChanges.join("; ")}`];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addedLines.length) changed.push(`billlines added: ${addedLines.join(" | ")}`);
|
||||||
|
if (removedLines.length) changed.push(`billlines removed: ${removedLines.join(" | ")}`);
|
||||||
|
if (modifiedLines.length) changed.push(`billlines modified: ${modifiedLines.join(" | ")}`);
|
||||||
|
|
||||||
|
return changed.length ? changed.join("; ") : NO_CHANGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildJobLineInsertAuditDetails(values = {}) {
|
||||||
|
const details = Object.entries(values)
|
||||||
|
.filter(([key, value]) => !JOB_LINE_SKIP_KEYS.has(key) && !isBlank(value))
|
||||||
|
.map(([key, value]) => `${key}: ${formatJobLineValue(key, value)}`);
|
||||||
|
|
||||||
|
return details.length ? details.join("; ") : NO_CHANGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildJobLineUpdateAuditDetails({ originalLine = {}, values = {} }) {
|
||||||
|
const details = buildFieldChangeDetails({
|
||||||
|
keys: Object.keys(values),
|
||||||
|
original: originalLine,
|
||||||
|
updated: values,
|
||||||
|
displayValue: formatJobLineValue,
|
||||||
|
skippedKeys: JOB_LINE_SKIP_KEYS
|
||||||
|
});
|
||||||
|
|
||||||
|
return details.length ? details.join("; ") : NO_CHANGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTimeTicketAuditSummary({ originalTicket = {}, submittedValues = {}, employees = [] }) {
|
||||||
|
const normalizedOriginal = {
|
||||||
|
...originalTicket,
|
||||||
|
jobid: originalTicket.job?.id ?? originalTicket.jobid ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
const details = buildFieldChangeDetails({
|
||||||
|
keys: Object.keys(submittedValues),
|
||||||
|
original: normalizedOriginal,
|
||||||
|
updated: submittedValues,
|
||||||
|
displayValue: (key, value) =>
|
||||||
|
formatTimeTicketValue(key, value, {
|
||||||
|
employees,
|
||||||
|
fallbackEmployee: key === "employeeid" ? normalizedOriginal.employee : null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const employeeName = getEmployeeName(
|
||||||
|
submittedValues.employeeid ?? normalizedOriginal.employeeid,
|
||||||
|
employees,
|
||||||
|
normalizedOriginal.employee
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: formatDate(submittedValues.date ?? normalizedOriginal.date),
|
||||||
|
details: details.length ? details.join("; ") : NO_CHANGES,
|
||||||
|
employeeName,
|
||||||
|
jobid: submittedValues.jobid ?? normalizedOriginal.jobid ?? null
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
comment: Project Mexico
|
comment: Project Mexico
|
||||||
|
- name: Chatter API Data Pump
|
||||||
|
webhook: '{{HASURA_API_URL}}/data/chatter-api'
|
||||||
|
schedule: 45 4 * * *
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: x-imex-auth
|
||||||
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
comment: ""
|
||||||
- name: Chatter Data Pump
|
- name: Chatter Data Pump
|
||||||
webhook: '{{HASURA_API_URL}}/data/chatter'
|
webhook: '{{HASURA_API_URL}}/data/chatter'
|
||||||
schedule: 45 5 * * *
|
schedule: 45 5 * * *
|
||||||
|
|||||||
@@ -1164,6 +1164,7 @@
|
|||||||
- notification_followers
|
- notification_followers
|
||||||
- state
|
- state
|
||||||
- md_order_statuses
|
- md_order_statuses
|
||||||
|
- md_ro_statuses
|
||||||
retry_conf:
|
retry_conf:
|
||||||
interval_sec: 10
|
interval_sec: 10
|
||||||
num_retries: 0
|
num_retries: 0
|
||||||
@@ -1184,7 +1185,8 @@
|
|||||||
"new": {
|
"new": {
|
||||||
"id": {{$body.event.data.new.id}},
|
"id": {{$body.event.data.new.id}},
|
||||||
"shopname": {{$body.event.data.new.shopname}},
|
"shopname": {{$body.event.data.new.shopname}},
|
||||||
"md_order_statuses": {{$body.event.data.new.md_order_statuses}}
|
"md_order_statuses": {{$body.event.data.new.md_order_statuses}},
|
||||||
|
"md_ro_statuses": {{$body.event.data.new.md_ro_statuses}}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"op": {{$body.event.op}},
|
"op": {{$body.event.op}},
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ awslocal ses verify-email-identity --email-address noreply@imex.online --region
|
|||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
ensure_secret_file "CHATTER_PRIVATE_KEY" "/tmp/certs/io-ftp-test.key"
|
ensure_secret_file "CHATTER_PRIVATE_KEY" "/tmp/certs/io-ftp-test.key"
|
||||||
ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713:-REPLACE_ME}"
|
ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713}"
|
||||||
|
ensure_secret_string "CHATTER_COMPANY_KEY_6746" "${CHATTER_COMPANY_KEY_6746}"
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
ensure_log_group "development"
|
ensure_log_group "development"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ async function OpenSearchUpdateHandler(req, res) {
|
|||||||
clm_no
|
clm_no
|
||||||
clm_total
|
clm_total
|
||||||
comment
|
comment
|
||||||
|
dms_id
|
||||||
ins_co_nm
|
ins_co_nm
|
||||||
owner_owing
|
owner_owing
|
||||||
ownr_co_nm
|
ownr_co_nm
|
||||||
|
|||||||
843
package-lock.json
generated
843
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user