diff --git a/client/package-lock.json b/client/package-lock.json index 568cc6058..225d2ba06 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 8b88d8e43..b0cbdf6b8 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/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx index 584aac402..e35200a74 100644 --- a/client/src/components/production-list-table/production-list-table.component.jsx +++ b/client/src/components/production-list-table/production-list-table.component.jsx @@ -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 { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import _ from "lodash"; import { useEffect, useMemo, useRef, useState } from "react"; -import ReactDragListView from "react-drag-listview"; +import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors, closestCenter } 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 { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { selectDarkMode } from "../../redux/application/application.selectors.js"; import Prompt from "../../utils/prompt.js"; import AlertComponent from "../alert/alert.component.jsx"; import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component"; @@ -23,12 +27,108 @@ import { logImEXEvent } from "../../firebase/firebase.utils.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, 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, + cursor: "move", + opacity: isDragging ? 0.3 : 1 + }; + + // If no columnKey, render as regular header + if (!columnKey) { + return ; + } + + // Combine drag functionality with resize + return ( + + {children} + + ); +} + +// Draggable column title component +// function DraggableColumnTitle({ col, onRemove, t }) { +// const { attributes, listeners, setNodeRef, isDragging } = useSortable({ +// id: col.key +// }); +// +// const menu = { +// onClick: onRemove, +// items: [ +// { +// key: col.key, +// label: t("production.actions.removecolumn") +// } +// ] +// }; +// +// const style = { +// display: "flex", +// alignItems: "center", +// width: "100%", +// opacity: isDragging ? 0.4 : 1 +// }; +// +// return ( +//
+// +// +// +// +// {col.title} +// +//
+// ); +// } + +export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser, isDarkMode }) { const [searchText, setSearchText] = useState(""); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [activeId, setActiveId] = useState(null); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 1 + } + }) + ); const { treatments: { Production_List_Status_Colors, Enhanced_Payroll } } = useTreatmentsWithConfig({ @@ -118,17 +218,30 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici logImEXEvent("production_list_sort_filter", { pagination, filters, sorter }); }; - const onDragEnd = (fromIndex, toIndex) => { - if (fromIndex === toIndex) return; - const columnsCopy = [...columns]; - const [movedItem] = columnsCopy.splice(fromIndex, 1); - columnsCopy.splice(toIndex, 0, movedItem); - if (!_.isEqual(columnsCopy, columns)) { - setColumns(columnsCopy); - setHasUnsavedChanges(true); + const onDragStart = ({ active }) => { + setActiveId(active.id); + }; + + const onDragEnd = ({ active, over }) => { + setActiveId(null); + if (!over || active.id === over.id) return; + + 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 { key } = e; const newColumns = columns.filter((i) => i.key !== key); @@ -172,10 +285,14 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici } ] }; + return ( - - {col.title} - +
+ + + {col.title} + +
); }; @@ -286,60 +403,99 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici } /> - - { - 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: { - "--bgColor": color.color - ? `rgba(${color.color.r},${color.color.g},${color.color.b},${color.color.a || 1})` - : "var(--status-row-bg-fallback)" + + 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: { + "--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, 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) => ({ + columnKey: column.key, + width: column.width, + onResize: handleResize(index) + }) }; - } - })} - 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: 1000 }} + 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..023a4ce9e 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,10 +1,11 @@ +import { forwardRef } from "react"; import { Resizable } from "react-resizable"; -export default function ResizableComponent(props) { - const { onResize, width, ...restProps } = props; +const ResizableComponent = forwardRef((props, ref) => { + const { onResize, width, dragAttributes, dragListeners, ...restProps } = props; if (!width) { - return
; + return ; } return ( @@ -22,7 +23,11 @@ export default function ResizableComponent(props) { /> } > - + ); -} +}); + +ResizableComponent.displayName = "ResizableComponent"; + +export default ResizableComponent;