diff --git a/client/package-lock.json b/client/package-lock.json index 5d3e81dac..8d567e72f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,6 +12,10 @@ "@amplitude/analytics-browser": "^2.34.0", "@ant-design/pro-layout": "^7.22.6", "@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", "@fingerprintjs/fingerprintjs": "^5.0.1", "@firebase/analytics": "^0.10.19", @@ -60,7 +64,6 @@ "react-color": "^2.19.3", "react-cookie": "^8.0.1", "react-dom": "^19.2.4", - "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.2", "react-i18next": "^16.5.4", @@ -2494,6 +2497,73 @@ "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": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz", @@ -8396,16 +8466,6 @@ "@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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -9192,14 +9252,6 @@ "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": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", @@ -15376,16 +15428,6 @@ "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": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", @@ -15972,12 +16014,6 @@ "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": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", diff --git a/client/package.json b/client/package.json index 81faa3245..b51becacf 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,10 @@ "@amplitude/analytics-browser": "^2.34.0", "@ant-design/pro-layout": "^7.22.6", "@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", "@fingerprintjs/fingerprintjs": "^5.0.1", "@firebase/analytics": "^0.10.19", @@ -59,7 +63,6 @@ "react-color": "^2.19.3", "react-cookie": "^8.0.1", "react-dom": "^19.2.4", - "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "^2.2.2", "react-i18next": "^16.5.4", diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 09745eac9..657520e49 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -446,3 +446,32 @@ //.rbc-time-header-gutter { // 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; +} diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index ed7bf7457..fb1b7a2cd 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -1,6 +1,7 @@ import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -32,6 +33,7 @@ export function BillEnterModalLinesComponent({ }) { const { t } = useTranslation(); const { setFieldsValue, getFieldsValue, getFieldValue } = form; + const firstFieldRefs = useRef({}); const CONTROL_HEIGHT = 32; @@ -155,6 +157,9 @@ export function BillEnterModalLinesComponent({ ), formInput: (record, index) => ( { + firstFieldRefs.current[index] = el; + }} disabled={disabled} options={lineData} style={{ @@ -205,7 +210,7 @@ export function BillEnterModalLinesComponent({ label: t("billlines.fields.line_desc"), rules: [{ required: true }] }), - formInput: () => + formInput: () => }, { title: t("billlines.fields.quantity"), @@ -234,7 +239,7 @@ export function BillEnterModalLinesComponent({ }) ] }), - formInput: () => + formInput: () => }, { title: t("billlines.fields.actual_price"), @@ -251,6 +256,7 @@ export function BillEnterModalLinesComponent({ { if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index); @@ -328,6 +334,7 @@ export function BillEnterModalLinesComponent({ min={0} disabled={disabled} controls={false} + tabIndex={0} style={{ width: "100%", height: CONTROL_HEIGHT }} // NOTE: No auto-fill on focus/blur; only triggered from Retail on Tab /> @@ -396,6 +403,7 @@ export function BillEnterModalLinesComponent({ showSearch style={{ minWidth: "3rem" }} disabled={disabled} + tabIndex={0} options={ bodyshopHasDmsKey(bodyshop) ? CiecaSelect(true, false) @@ -419,6 +427,7 @@ export function BillEnterModalLinesComponent({ formInput: () => ( - - { - 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) + + col.key)} strategy={horizontalListSortingStrategy}> +
{ + 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 { + className: "rowWithColor", 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) + }) }; - } - })} - components={{ - header: { - cell: ResizeableTitle - } - }} - columns={columns.map((c, index) => { - 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) => ({ - width: column.width, - onResize: handleResize(index) - }) - }; - })} - rowKey="id" - loading={loading} - dataSource={dataSource} - scroll={{ x: 1000 }} - onChange={handleTableChange} - /> - + })} + rowKey="id" + loading={loading} + dataSource={dataSource} + scroll={{ x: scrollX }} + onChange={handleTableChange} + /> + + + + {activeId ? ( +
+ + + {(() => { + const col = columns.find((c) => c.key === activeId); + const title = typeof col?.title === "string" ? col.title : col?.dataIndex || col?.key || "Column"; + return title; + })()} + +
+ ) : null} +
+ ); } diff --git a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx index bfb634228..15d11906c 100644 --- a/client/src/components/production-list-table/production-list-table.resizeable.component.jsx +++ b/client/src/components/production-list-table/production-list-table.resizeable.component.jsx @@ -1,28 +1,37 @@ +import { forwardRef } from "react"; import { Resizable } from "react-resizable"; +import "react-resizable/css/styles.css"; -export default function ResizableComponent(props) { - const { onResize, width, ...restProps } = props; +const ResizableComponent = forwardRef((props, ref) => { + const { onResize, onResizeStart, onResizeStop, width, dragAttributes, dragListeners, ...restProps } = props; if (!width) { - return
; + return ; } return ( ( { - e.stopPropagation(); - }} + ref={handleRef} + className={`react-resizable-handle react-resizable-handle-${axis}`} + onClick={(e) => e.stopPropagation()} /> - } + )} > - + ); -} +}); + +ResizableComponent.displayName = "ResizableComponent"; + +export default ResizableComponent; diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 19138c5c0..819c1d292 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -221,6 +221,8 @@ const CreateRepairOrderTag = (job, errorCallback) => { const repairCosts = CreateCosts(job); + const LaborDetailLines = generateLaborLines(job.timetickets); + //Calculate detail only lines. const detailAdjustments = job.joblines .filter((jl) => jl.ah_detail_line && jl.mod_lbr_ty) @@ -606,12 +608,14 @@ const CreateRepairOrderTag = (job, errorCallback) => { // CSIID: null, InsGroupCode: null }, - DetailLines: { DetailLine: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(job, jl, job.bodyshop.md_order_statuses)) : [generateNullDetailLine()] + }, + LaborDetailLines: { + LaborDetailLine: LaborDetailLines } }; return ret; @@ -787,6 +791,76 @@ const CreateCosts = (job) => { }; }; +const generateLaborLines = (timetickets) => { + if (!timetickets || timetickets.length === 0) return []; + + const codeToProps = { + LAB: { actual: "LaborBodyActualHours", flag: "LaborBodyFlagHours", cost: "LaborBodyCost" }, + LAM: { actual: "LaborMechanicalActualHours", flag: "LaborMechanicalFlagHours", cost: "LaborMechanicalCost" }, + LAG: { actual: "LaborGlassActualHours", flag: "LaborGlassFlagHours", cost: "LaborGlassCost" }, + LAS: { actual: "LaborStructuralActualHours", flag: "LaborStructuralFlagHours", cost: "LaborStructuralCost" }, + LAE: { actual: "LaborElectricalActualHours", flag: "LaborElectricalFlagHours", cost: "LaborElectricalCost" }, + LAA: { actual: "LaborAluminumActualHours", flag: "LaborAluminumFlagHours", cost: "LaborAluminumCost" }, + LAR: { actual: "LaborRefinishActualHours", flag: "LaborRefinishFlagHours", cost: "LaborRefinishCost" }, + LAU: { actual: "LaborDetailActualHours", flag: "LaborDetailFlagHours", cost: "LaborDetailCost" }, + LA1: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" }, + LA2: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" }, + LA3: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" }, + LA4: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" } + }; + + return timetickets.map((ticket, idx) => { + const { ciecacode, employee, actualhrs = 0, productivehrs = 0, rate = 0 } = ticket; + const isFlatRate = employee?.flat_rate; + const hours = isFlatRate ? productivehrs : actualhrs; + const cost = rate * hours; + + const laborDetail = { + LaborDetailLineNumber: idx + 1, + TechnicianNameFirst: employee?.first_name || "", + TechnicianNameLast: employee?.last_name || "", + LaborBodyActualHours: 0, + LaborMechanicalActualHours: 0, + LaborGlassActualHours: 0, + LaborStructuralActualHours: 0, + LaborElectricalActualHours: 0, + LaborAluminumActualHours: 0, + LaborRefinishActualHours: 0, + LaborDetailActualHours: 0, + LaborOtherActualHours: 0, + LaborBodyFlagHours: 0, + LaborMechanicalFlagHours: 0, + LaborGlassFlagHours: 0, + LaborStructuralFlagHours: 0, + LaborElectricalFlagHours: 0, + LaborAluminumFlagHours: 0, + LaborRefinishFlagHours: 0, + LaborDetailFlagHours: 0, + LaborOtherFlagHours: 0, + LaborBodyCost: 0, + LaborMechanicalCost: 0, + LaborGlassCost: 0, + LaborStructuralCost: 0, + LaborElectricalCost: 0, + LaborAluminumCost: 0, + LaborRefinishCost: 0, + LaborDetailCost: 0, + LaborOtherCost: 0 + }; + + const effectiveCiecacode = ciecacode || "LA4"; + + if (codeToProps[effectiveCiecacode]) { + const { actual, flag, cost: costProp } = codeToProps[effectiveCiecacode]; + laborDetail[actual] = actualhrs; + laborDetail[flag] = productivehrs; + laborDetail[costProp] = cost; + } + + return laborDetail; + }); +}; + const StatusMapping = (status, md_ro_statuses) => { //Possible return statuses EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED. const { diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 54714f723..63a0f00d7 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -827,13 +827,21 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop quantity } } - timetickets { + timetickets(where: {cost_center: {_neq: "timetickets.labels.shift"}}) { id rate + ciecacode cost_center actualhrs productivehrs flat_rate + employeeid + employee { + employee_number + flat_rate + first_name + last_name + } } area_of_damage employee_prep_rel {