Merged in hotfix/2026-02-03 (pull request #2965)
Hotfix/2026 02 03 Approved-by: Allan Carr
This commit is contained in:
106
client/package-lock.json
generated
106
client/package-lock.json
generated
@@ -12,6 +12,10 @@
|
|||||||
"@amplitude/analytics-browser": "^2.34.0",
|
"@amplitude/analytics-browser": "^2.34.0",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.3",
|
"@apollo/client": "^4.1.3",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||||
"@firebase/analytics": "^0.10.19",
|
"@firebase/analytics": "^0.10.19",
|
||||||
@@ -60,7 +64,6 @@
|
|||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-drag-listview": "^2.0.0",
|
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-i18next": "^16.5.4",
|
"react-i18next": "^16.5.4",
|
||||||
@@ -2494,6 +2497,73 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/modifiers": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dotenvx/dotenvx": {
|
"node_modules/@dotenvx/dotenvx": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz",
|
||||||
@@ -8396,16 +8466,6 @@
|
|||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-runtime": {
|
|
||||||
"version": "6.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
|
||||||
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"core-js": "^2.4.0",
|
|
||||||
"regenerator-runtime": "^0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||||
@@ -9192,14 +9252,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
|
||||||
"version": "2.6.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
|
||||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
|
||||||
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.47.0",
|
"version": "3.47.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz",
|
||||||
@@ -15376,16 +15428,6 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-drag-listview": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-drag-listview/-/react-drag-listview-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-7Apx/1Xt4qu+JHHP0rH6aLgZgS7c2MX8ocHVGCi03KfeIWEu0t14MhT3boQKM33l5eJrE/IWfExFTvoYq22fsg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"prop-types": "^15.5.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-draggable": {
|
"node_modules/react-draggable": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
||||||
@@ -15972,12 +16014,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regenerator-runtime": {
|
|
||||||
"version": "0.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
|
||||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
"@amplitude/analytics-browser": "^2.34.0",
|
"@amplitude/analytics-browser": "^2.34.0",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.3",
|
"@apollo/client": "^4.1.3",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||||
"@firebase/analytics": "^0.10.19",
|
"@firebase/analytics": "^0.10.19",
|
||||||
@@ -59,7 +63,6 @@
|
|||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.0.1",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-drag-listview": "^2.0.0",
|
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-i18next": "^16.5.4",
|
"react-i18next": "^16.5.4",
|
||||||
|
|||||||
@@ -446,3 +446,32 @@
|
|||||||
//.rbc-time-header-gutter {
|
//.rbc-time-header-gutter {
|
||||||
// padding: 0;
|
// padding: 0;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
/* globally allow shrink inside table cells */
|
||||||
|
.prod-list-table .ant-table-cell,
|
||||||
|
.prod-list-table .ant-table-cell > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* common AntD offenders */
|
||||||
|
.prod-list-table > .ant-table-cell .ant-space,
|
||||||
|
.ant-table-cell .ant-space-item {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep your custom header content on the left, push AntD sorter to the far right */
|
||||||
|
.prod-list-table .ant-table-column-sorters {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prod-list-table .ant-table-column-title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0; /* allows ellipsis to work */
|
||||||
|
}
|
||||||
|
|
||||||
|
.prod-list-table .ant-table-column-sorter {
|
||||||
|
margin-left: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
||||||
|
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 { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -32,6 +33,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
|
const firstFieldRefs = useRef({});
|
||||||
|
|
||||||
const CONTROL_HEIGHT = 32;
|
const CONTROL_HEIGHT = 32;
|
||||||
|
|
||||||
@@ -155,6 +157,9 @@ export function BillEnterModalLinesComponent({
|
|||||||
),
|
),
|
||||||
formInput: (record, index) => (
|
formInput: (record, index) => (
|
||||||
<BillLineSearchSelect
|
<BillLineSearchSelect
|
||||||
|
ref={(el) => {
|
||||||
|
firstFieldRefs.current[index] = el;
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={lineData}
|
options={lineData}
|
||||||
style={{
|
style={{
|
||||||
@@ -205,7 +210,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
label: t("billlines.fields.line_desc"),
|
label: t("billlines.fields.line_desc"),
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
}),
|
}),
|
||||||
formInput: () => <Input.TextArea disabled={disabled} autoSize />
|
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.quantity"),
|
title: t("billlines.fields.quantity"),
|
||||||
@@ -234,7 +239,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
|
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.actual_price"),
|
title: t("billlines.fields.actual_price"),
|
||||||
@@ -251,6 +256,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
min={0}
|
min={0}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
tabIndex={0}
|
||||||
// NOTE: Autofill should only happen on forward Tab out of Retail
|
// NOTE: Autofill should only happen on forward Tab out of Retail
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
|
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
|
||||||
@@ -328,6 +334,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
min={0}
|
min={0}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
controls={false}
|
controls={false}
|
||||||
|
tabIndex={0}
|
||||||
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
||||||
// NOTE: No auto-fill on focus/blur; only triggered from Retail on Tab
|
// NOTE: No auto-fill on focus/blur; only triggered from Retail on Tab
|
||||||
/>
|
/>
|
||||||
@@ -392,7 +399,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
}),
|
}),
|
||||||
formInput: () => (
|
formInput: () => (
|
||||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
|
||||||
{bodyshopHasDmsKey(bodyshop)
|
{bodyshopHasDmsKey(bodyshop)
|
||||||
? CiecaSelect(true, false)
|
? CiecaSelect(true, false)
|
||||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||||
@@ -412,7 +419,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
name: [field.name, "location"]
|
name: [field.name, "location"]
|
||||||
}),
|
}),
|
||||||
formInput: () => (
|
formInput: () => (
|
||||||
<Select disabled={disabled}>
|
<Select disabled={disabled} tabIndex={0}>
|
||||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||||
<Select.Option key={idx} value={loc}>
|
<Select.Option key={idx} value={loc}>
|
||||||
{loc}
|
{loc}
|
||||||
@@ -432,7 +439,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
key: `${field.name}deductedfromlbr`,
|
key: `${field.name}deductedfromlbr`,
|
||||||
name: [field.name, "deductedfromlbr"]
|
name: [field.name, "deductedfromlbr"]
|
||||||
}),
|
}),
|
||||||
formInput: () => <Switch disabled={disabled} />,
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />,
|
||||||
additional: (record, index) => (
|
additional: (record, index) => (
|
||||||
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
|
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
|
||||||
{() => {
|
{() => {
|
||||||
@@ -523,7 +530,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
rome: false
|
rome: false
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
formInput: () => <Switch disabled={disabled} />
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
@@ -538,7 +545,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
valuePropName: "checked",
|
valuePropName: "checked",
|
||||||
name: [field.name, "applicable_taxes", "state"]
|
name: [field.name, "applicable_taxes", "state"]
|
||||||
}),
|
}),
|
||||||
formInput: () => <Switch disabled={disabled} />
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||||
},
|
},
|
||||||
|
|
||||||
...InstanceRenderManager({
|
...InstanceRenderManager({
|
||||||
@@ -554,7 +561,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
valuePropName: "checked",
|
valuePropName: "checked",
|
||||||
name: [field.name, "applicable_taxes", "local"]
|
name: [field.name, "applicable_taxes", "local"]
|
||||||
}),
|
}),
|
||||||
formInput: () => <Switch disabled={disabled} />
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
@@ -574,6 +581,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
icon={<DeleteFilled />}
|
icon={<DeleteFilled />}
|
||||||
disabled={disabled || invLen > 0}
|
disabled={disabled || invLen > 0}
|
||||||
onClick={() => remove(record.name)}
|
onClick={() => remove(record.name)}
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{Simple_Inventory.treatment === "on" && (
|
{Simple_Inventory.treatment === "on" && (
|
||||||
@@ -645,12 +653,19 @@ export function BillEnterModalLinesComponent({
|
|||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const newIndex = fields.length;
|
||||||
add(
|
add(
|
||||||
InstanceRenderManager({
|
InstanceRenderManager({
|
||||||
imex: { applicable_taxes: { federal: true } },
|
imex: { applicable_taxes: { federal: true } },
|
||||||
rome: { applicable_taxes: { federal: false } }
|
rome: { applicable_taxes: { federal: false } }
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstField = firstFieldRefs.current[newIndex];
|
||||||
|
if (firstField?.focus) {
|
||||||
|
firstField.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -132,7 +132,13 @@ export function LaborAllocationsAdjustmentEdit({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={(vis) => setOpen(vis)} content={overlay} trigger="click">
|
<Popover
|
||||||
|
getPopupContainer={(trigger) => trigger?.parentElement || document.body}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(vis) => setOpen(vis)}
|
||||||
|
content={overlay}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
|
|
||||||
const result = await updateJob({
|
const result = await updateJob({
|
||||||
variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
|
variables: { jobId: record.id, job: { [empAssignment]: employeeid } }
|
||||||
|
|
||||||
// awaitRefetchQueries: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -55,6 +53,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|
||||||
|
setAssignment({ operation: null, employeeid: null });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
const handleRemove = async (operation) => {
|
const handleRemove = async (operation) => {
|
||||||
@@ -84,6 +83,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|
||||||
|
setAssignment({ operation: null, employeeid: null });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,29 +94,31 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
|
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
const onChange = (e, option) => {
|
const onChange = (e, option) => {
|
||||||
setAssignment({ ...assignment, employeeid: e, name: option.name });
|
setAssignment({ ...assignment, employeeid: e, name: option.label });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const employeeOptions = bodyshop.employees
|
||||||
|
.filter((emp) => emp.active)
|
||||||
|
.map((emp) => ({
|
||||||
|
value: emp.id,
|
||||||
|
label: `${emp.first_name} ${emp.last_name}`,
|
||||||
|
name: `${emp.first_name} ${emp.last_name}`
|
||||||
|
}));
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Select
|
<Select
|
||||||
id="employeeSelector"
|
id="employeeSelector"
|
||||||
showSearch={{
|
showSearch={{
|
||||||
optionFilterProp: "children",
|
optionFilterProp: "label",
|
||||||
filterOption: (input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
filterOption: (input, option) => option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
}}
|
}}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
|
value={assignment.employeeid}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
options={employeeOptions}
|
||||||
{bodyshop.employees
|
/>
|
||||||
.filter((emp) => emp.active)
|
|
||||||
.map((emp) => (
|
|
||||||
<Select.Option value={emp.id} key={emp.id} name={`${emp.first_name} ${emp.last_name}`}>
|
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
@@ -141,25 +143,25 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
|||||||
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover destroyOnHidden content={popContent} open={visibility}>
|
<Spin spinning={loading}>
|
||||||
<Spin spinning={loading}>
|
{record[type] ? (
|
||||||
{record[type] ? (
|
<div style={{ cursor: "pointer" }}>
|
||||||
<div style={{ cursor: "pointer" }}>
|
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
||||||
<span>{`${theEmployee?.first_name || ""} ${theEmployee?.last_name || ""}`}</span>
|
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
||||||
<DeleteFilled style={iconStyle} onClick={() => handleRemove(type)} />
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<Popover destroyOnHidden content={popContent} open={visibility} trigger="click">
|
||||||
<PlusCircleFilled
|
<PlusCircleFilled
|
||||||
style={{ ...iconStyle, cursor: "pointer" }}
|
style={{ ...iconStyle, cursor: "pointer" }}
|
||||||
className="muted-button"
|
className="muted-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAssignment({ operation: type });
|
setAssignment({ operation: type, employeeid: null });
|
||||||
setVisibility(true);
|
setVisibility(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</Spin>
|
)}
|
||||||
</Popover>
|
</Spin>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ReactDragListView from "react-drag-listview";
|
import { closestCenter, DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||||
|
import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||||
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 { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
import Prompt from "../../utils/prompt.js";
|
import Prompt from "../../utils/prompt.js";
|
||||||
import AlertComponent from "../alert/alert.component.jsx";
|
import AlertComponent from "../alert/alert.component.jsx";
|
||||||
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
|
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
|
||||||
@@ -23,12 +27,81 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
technician: selectTechnician,
|
technician: selectTechnician,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
isDarkMode: selectDarkMode
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
|
// Draggable header cell component - combines drag and resize
|
||||||
|
function DraggableHeaderCell(props) {
|
||||||
|
const { children, columnKey, onResize, width, ...restProps } = props;
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
|
id: columnKey,
|
||||||
|
disabled: !columnKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
...restProps.style,
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.3 : 1,
|
||||||
|
userSelect: "none",
|
||||||
|
textAlign: "left"
|
||||||
|
};
|
||||||
|
|
||||||
|
// If no columnKey, render as regular header
|
||||||
|
if (!columnKey) {
|
||||||
|
return <ResizeableTitle {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only apply drag listeners to elements with data-drag-handle attribute
|
||||||
|
const filteredListeners = listeners
|
||||||
|
? {
|
||||||
|
onPointerDown: (e) => {
|
||||||
|
// Only trigger drag if clicking on the drag handle
|
||||||
|
if (e.target.closest('[data-drag-handle="true"]')) {
|
||||||
|
listeners.onPointerDown?.(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Combine drag functionality with resize
|
||||||
|
return (
|
||||||
|
<ResizeableTitle
|
||||||
|
{...restProps}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
onResize={onResize}
|
||||||
|
width={width}
|
||||||
|
dragAttributes={attributes}
|
||||||
|
dragListeners={filteredListeners}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ResizeableTitle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
|
||||||
|
// NEW: smoother resize
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const resizeRafRef = useRef(null);
|
||||||
|
const pendingResizeRef = useRef(null);
|
||||||
|
|
||||||
|
const [activeId, setActiveId] = useState(null);
|
||||||
|
|
||||||
|
const MIN_COL_WIDTH = 20;
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -36,8 +109,10 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
|
names: ["Production_List_Status_Colors", "Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
||||||
const defaultView = assoc?.default_prod_list_view;
|
const defaultView = assoc?.default_prod_list_view;
|
||||||
|
|
||||||
const initialStateRef = useRef(
|
const initialStateRef = useRef(
|
||||||
(bodyshop.production_config &&
|
(bodyshop.production_config &&
|
||||||
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
||||||
@@ -46,6 +121,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
filteredInfo: { text: "" }
|
filteredInfo: { text: "" }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialColumnsRef = useRef(
|
const initialColumnsRef = useRef(
|
||||||
(initialStateRef.current &&
|
(initialStateRef.current &&
|
||||||
bodyshop?.production_config
|
bodyshop?.production_config
|
||||||
@@ -66,14 +142,36 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
})) ||
|
})) ||
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [state, setState] = useState(initialStateRef.current);
|
const [state, setState] = useState(initialStateRef.current);
|
||||||
const [columns, setColumns] = useState(initialColumnsRef.current);
|
const [columns, setColumns] = useState(initialColumnsRef.current);
|
||||||
|
|
||||||
|
const scrollX = useMemo(() => {
|
||||||
|
// keep scroll width aligned with the actual column widths so AntD doesn't clamp at a fixed floor
|
||||||
|
const sum = columns.reduce((acc, c) => acc + (c.width ?? 100), 0);
|
||||||
|
return Math.max(sum, 1);
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const matchingColumnConfig = useMemo(() => {
|
const matchingColumnConfig = useMemo(() => {
|
||||||
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
return bodyshop?.production_config?.find((p) => p.name === defaultView);
|
||||||
}, [bodyshop.production_config, defaultView]);
|
}, [bodyshop.production_config, defaultView]);
|
||||||
|
|
||||||
|
// NEW: cleanup RAF on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (resizeRafRef.current) cancelAnimationFrame(resizeRafRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// NEW: while resizing, don’t regenerate columns
|
||||||
|
if (isResizing) return;
|
||||||
|
|
||||||
|
// NEW: bail early BEFORE expensive ProductionListColumns(...) call
|
||||||
|
if (!_.isEqual(initialColumnsRef.current, columns)) return;
|
||||||
|
|
||||||
const newColumns =
|
const newColumns =
|
||||||
matchingColumnConfig?.columns.columnKeys.map((k) => {
|
matchingColumnConfig?.columns.columnKeys.map((k) => {
|
||||||
return {
|
return {
|
||||||
@@ -89,10 +187,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
width: k.width ?? 100
|
width: k.width ?? 100
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
// Only update columns if they haven't been manually changed by the user
|
|
||||||
if (_.isEqual(initialColumnsRef.current, columns)) {
|
setColumns(newColumns);
|
||||||
setColumns(newColumns);
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
matchingColumnConfig,
|
matchingColumnConfig,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
@@ -102,7 +198,8 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
Production_List_Status_Colors,
|
Production_List_Status_Colors,
|
||||||
refetch,
|
refetch,
|
||||||
state,
|
state,
|
||||||
columns
|
columns,
|
||||||
|
isResizing
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
@@ -118,17 +215,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
|
logImEXEvent("production_list_sort_filter", { pagination, filters, sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (fromIndex, toIndex) => {
|
const onDragStart = ({ active }) => {
|
||||||
if (fromIndex === toIndex) return;
|
setActiveId(active.id);
|
||||||
const columnsCopy = [...columns];
|
};
|
||||||
const [movedItem] = columnsCopy.splice(fromIndex, 1);
|
|
||||||
columnsCopy.splice(toIndex, 0, movedItem);
|
const onDragEnd = ({ active, over }) => {
|
||||||
if (!_.isEqual(columnsCopy, columns)) {
|
setActiveId(null);
|
||||||
setColumns(columnsCopy);
|
if (!over || active.id === over.id) return;
|
||||||
setHasUnsavedChanges(true);
|
|
||||||
|
const oldIndex = columns.findIndex((col) => col.key === active.id);
|
||||||
|
const newIndex = columns.findIndex((col) => col.key === over.id);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
const newColumns = arrayMove(columns, oldIndex, newIndex);
|
||||||
|
if (!_.isEqual(newColumns, columns)) {
|
||||||
|
setColumns(newColumns);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDragCancel = () => {
|
||||||
|
setActiveId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const removeColumn = (e) => {
|
const removeColumn = (e) => {
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
const newColumns = columns.filter((i) => i.key !== key);
|
const newColumns = columns.filter((i) => i.key !== key);
|
||||||
@@ -139,19 +249,55 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
logImEXEvent("production_list_remove_column", { key });
|
logImEXEvent("production_list_remove_column", { key });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize =
|
// NEW: commit widths via rAF (less jank)
|
||||||
(index) =>
|
const applyColumnWidth = useCallback((columnKey, width) => {
|
||||||
(e, { size }) => {
|
const nextWidth = Math.max(MIN_COL_WIDTH, Math.round(width));
|
||||||
const nextColumns = [...columns];
|
setColumns((prev) => {
|
||||||
nextColumns[index] = {
|
const idx = prev.findIndex((c) => c.key === columnKey);
|
||||||
...nextColumns[index],
|
if (idx === -1) return prev;
|
||||||
width: size.width
|
|
||||||
};
|
const currentWidth = prev[idx].width ?? 100;
|
||||||
if (!_.isEqual(nextColumns, columns)) {
|
if (currentWidth === nextWidth) return prev;
|
||||||
setColumns(nextColumns);
|
|
||||||
|
const next = prev.slice();
|
||||||
|
next[idx] = { ...next[idx], width: nextWidth };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResize = useCallback(
|
||||||
|
(columnKey) =>
|
||||||
|
(e, { size }) => {
|
||||||
|
pendingResizeRef.current = { columnKey, width: size.width };
|
||||||
|
|
||||||
|
if (resizeRafRef.current) return;
|
||||||
|
resizeRafRef.current = requestAnimationFrame(() => {
|
||||||
|
resizeRafRef.current = null;
|
||||||
|
const pending = pendingResizeRef.current;
|
||||||
|
if (!pending) return;
|
||||||
|
applyColumnWidth(pending.columnKey, pending.width);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[applyColumnWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResizeStart = useCallback(() => {
|
||||||
|
setIsResizing(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleResizeStop = useCallback(
|
||||||
|
(columnKey) =>
|
||||||
|
(e, { size }) => {
|
||||||
|
setIsResizing(false);
|
||||||
|
|
||||||
|
// Ensure final width is committed
|
||||||
|
applyColumnWidth(columnKey, size.width);
|
||||||
|
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
}
|
logImEXEvent("production_list_resize_column", { key: columnKey, width: size.width });
|
||||||
};
|
},
|
||||||
|
[applyColumnWidth]
|
||||||
|
);
|
||||||
|
|
||||||
const addColumn = (newColumn) => {
|
const addColumn = (newColumn) => {
|
||||||
const updatedColumns = [...columns, newColumn];
|
const updatedColumns = [...columns, newColumn];
|
||||||
@@ -163,19 +309,53 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
};
|
};
|
||||||
|
|
||||||
const headerItem = (col) => {
|
const headerItem = (col) => {
|
||||||
const menu = {
|
const menu = { onClick: removeColumn, items: [{ key: col.key, label: t("production.actions.removecolumn") }] };
|
||||||
onClick: removeColumn,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: col.key,
|
|
||||||
label: t("production.actions.removecolumn")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
<div
|
||||||
<span>{col.title}</span>
|
style={{
|
||||||
</Dropdown>
|
display: "flex",
|
||||||
|
alignItems: "left",
|
||||||
|
width: "100%",
|
||||||
|
userSelect: "none",
|
||||||
|
minWidth: 0 // critical: allow the flex row to shrink
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="drag-handle-trigger"
|
||||||
|
data-drag-handle="true"
|
||||||
|
style={{
|
||||||
|
marginRight: 8,
|
||||||
|
color: "#999",
|
||||||
|
cursor: "grab",
|
||||||
|
padding: 4,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "left",
|
||||||
|
userSelect: "none",
|
||||||
|
flex: "0 0 auto"
|
||||||
|
}}
|
||||||
|
title="Drag to reorder column"
|
||||||
|
>
|
||||||
|
<HolderOutlined />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Dropdown className="prod-header-dropdown" menu={menu} trigger={["contextMenu"]}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
flex: "1 1 auto",
|
||||||
|
minWidth: 0, // critical: allow text to shrink
|
||||||
|
overflow: "hidden", // clip
|
||||||
|
textOverflow: "ellipsis", // show …
|
||||||
|
whiteSpace: "nowrap", // keep single line
|
||||||
|
cursor: "default",
|
||||||
|
userSelect: "none",
|
||||||
|
display: "block"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.title}
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -274,6 +454,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
onSave={() => {
|
onSave={() => {
|
||||||
setHasUnsavedChanges(false);
|
setHasUnsavedChanges(false);
|
||||||
initialStateRef.current = state;
|
initialStateRef.current = state;
|
||||||
|
|
||||||
|
// NEW: after saving, treat current columns as the baseline
|
||||||
|
initialColumnsRef.current = columns;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -286,60 +469,104 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ProductionListDetail jobs={dataSource} />
|
<ProductionListDetail jobs={dataSource} />
|
||||||
<ReactDragListView.DragColumn onDragEnd={onDragEnd} nodeSelector="th" handleSelector=".prod-header-dropdown">
|
<DndContext
|
||||||
<Table
|
sensors={sensors}
|
||||||
sticky
|
onDragStart={onDragStart}
|
||||||
pagination={false}
|
onDragEnd={onDragEnd}
|
||||||
size="small"
|
onDragCancel={onDragCancel}
|
||||||
{...(Production_List_Status_Colors.treatment === "on" && {
|
collisionDetection={closestCenter}
|
||||||
onRow: (record, index) => {
|
modifiers={[restrictToHorizontalAxis]}
|
||||||
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
>
|
||||||
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
<SortableContext items={columns.map((col) => col.key)} strategy={horizontalListSortingStrategy}>
|
||||||
if (!color) {
|
<Table
|
||||||
if (index % 2 === 0)
|
sticky
|
||||||
|
tableLayout="fixed"
|
||||||
|
className="prod-list-table"
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
{...(Production_List_Status_Colors.treatment === "on" &&
|
||||||
|
!isResizing && {
|
||||||
|
onRow: (record, index) => {
|
||||||
|
if (!bodyshop.md_ro_statuses.production_colors) return null;
|
||||||
|
const color = bodyshop.md_ro_statuses.production_colors.find((x) => x.status === record.status);
|
||||||
|
if (!color) {
|
||||||
|
if (index % 2 === 0)
|
||||||
|
return {
|
||||||
|
style: {
|
||||||
|
backgroundColor: "var(--table-row-even-bg)"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
|
className: "rowWithColor",
|
||||||
style: {
|
style: {
|
||||||
backgroundColor: "var(--table-row-even-bg)"
|
"--bgColor": color.color
|
||||||
|
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
|
||||||
|
: "var(--status-row-bg-fallback)"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
className: "rowWithColor",
|
|
||||||
style: {
|
|
||||||
"--bgColor": color.color
|
|
||||||
? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})`
|
|
||||||
: "var(--status-row-bg-fallback)"
|
|
||||||
}
|
}
|
||||||
|
})}
|
||||||
|
components={{
|
||||||
|
header: {
|
||||||
|
cell: DraggableHeaderCell
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns.map((c) => {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
filteredValue: state.filteredInfo[c.key] || null,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
||||||
|
title: headerItem(c),
|
||||||
|
ellipsis: true,
|
||||||
|
width: c.width ?? 100,
|
||||||
|
onHeaderCell: (column) => ({
|
||||||
|
columnKey: column.key,
|
||||||
|
width: column.width,
|
||||||
|
onResize: handleResize(column.key),
|
||||||
|
onResizeStart: handleResizeStart,
|
||||||
|
onResizeStop: handleResizeStop(column.key)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
})}
|
||||||
})}
|
rowKey="id"
|
||||||
components={{
|
loading={loading}
|
||||||
header: {
|
dataSource={dataSource}
|
||||||
cell: ResizeableTitle
|
scroll={{ x: scrollX }}
|
||||||
}
|
onChange={handleTableChange}
|
||||||
}}
|
/>
|
||||||
columns={columns.map((c, index) => {
|
</SortableContext>
|
||||||
return {
|
|
||||||
...c,
|
<DragOverlay dropAnimation={null}>
|
||||||
filteredValue: state.filteredInfo[c.key] || null,
|
{activeId ? (
|
||||||
sortOrder: state.sortedInfo.columnKey === c.key && state.sortedInfo.order,
|
<div
|
||||||
title: headerItem(c),
|
style={{
|
||||||
ellipsis: true,
|
backgroundColor: isDarkMode ? "#141414" : "white",
|
||||||
width: c.width ?? 100,
|
color: isDarkMode ? "white" : "#000",
|
||||||
onHeaderCell: (column) => ({
|
border: `2px solid ${isDarkMode ? "#177ddc" : "#1890ff"}`,
|
||||||
width: column.width,
|
borderRadius: "4px",
|
||||||
onResize: handleResize(index)
|
padding: "12px 16px",
|
||||||
})
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
|
||||||
};
|
cursor: "grabbing",
|
||||||
})}
|
display: "flex",
|
||||||
rowKey="id"
|
alignItems: "center",
|
||||||
loading={loading}
|
fontWeight: 500,
|
||||||
dataSource={dataSource}
|
minWidth: "120px"
|
||||||
scroll={{ x: 1000 }}
|
}}
|
||||||
onChange={handleTableChange}
|
>
|
||||||
/>
|
<HolderOutlined style={{ marginRight: "8px", color: isDarkMode ? "white" : "#000", fontSize: "16px" }} />
|
||||||
</ReactDragListView.DragColumn>
|
<span>
|
||||||
|
{(() => {
|
||||||
|
const col = columns.find((c) => c.key === activeId);
|
||||||
|
const title = typeof col?.title === "string" ? col.title : col?.dataIndex || col?.key || "Column";
|
||||||
|
return title;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,37 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
import { Resizable } from "react-resizable";
|
import { Resizable } from "react-resizable";
|
||||||
|
import "react-resizable/css/styles.css";
|
||||||
|
|
||||||
export default function ResizableComponent(props) {
|
const ResizableComponent = forwardRef((props, ref) => {
|
||||||
const { onResize, width, ...restProps } = props;
|
const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props;
|
||||||
|
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return <th {...restProps} />;
|
return <th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Resizable
|
<Resizable
|
||||||
width={width || 200}
|
width={width}
|
||||||
height={0}
|
height={0}
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
|
onResizeStart={onResizeStart}
|
||||||
|
onResizeStop={onResizeStop}
|
||||||
draggableOpts={{ enableUserSelectHack: false }}
|
draggableOpts={{ enableUserSelectHack: false }}
|
||||||
handle={
|
resizeHandles={["e"]}
|
||||||
|
axis="x"
|
||||||
|
handle={(axis, handleRef) => (
|
||||||
<span
|
<span
|
||||||
className="react-resizable-handle"
|
ref={handleRef}
|
||||||
onClick={(e) => {
|
className={`react-resizable-handle react-resizable-handle-${axis}`}
|
||||||
e.stopPropagation();
|
onClick={(e) => e.stopPropagation()}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<th {...restProps} />
|
<th ref={ref} {...restProps} {...(dragAttributes || {})} {...(dragListeners || {})} />
|
||||||
</Resizable>
|
</Resizable>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ResizableComponent.displayName = "ResizableComponent";
|
||||||
|
|
||||||
|
export default ResizableComponent;
|
||||||
|
|||||||
Reference in New Issue
Block a user