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 { ChromePicker } from "react-color"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; 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 { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./shop-info.color.utils"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoROStatusComponent); const SelectorDiv = styled.div` .ant-form-item .ant-select { 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 ( { event.stopPropagation(); }} onClick={(event) => { event.stopPropagation(); }} {...attributes} {...listeners} > { 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} > {labelText} {closable ? ( { event.stopPropagation(); onClose?.(event); }} onMouseDown={(event) => { event.stopPropagation(); }} > ) : null} ); }; 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 ; }; const statusSelectOptions = isTagsMode ? knownStatuses.map((status) => ({ value: status, label: status })) : options; if (statuses.length === 0) { return ( ); }; export function ShopInfoROStatusComponent({ bodyshop, form }) { 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 { treatments: { Production_List_Status_Colors } } = useTreatmentsWithConfig({ attributes: {}, names: ["Production_List_Status_Colors"], splitKey: bodyshop.imexshopid }); return (
{ const populatedStatuses = normalizeStatuses(value); if (populatedStatuses.length === 0) { return Promise.reject( new Error( t("general.validation.required", { label: t("bodyshop.labels.alljobstatuses") }) ) ); } if (populatedStatuses.length !== (value || []).filter(Boolean).length) { return Promise.reject(new Error(t("bodyshop.errors.duplicate_job_status"))); } } } ]} >
{Production_List_Status_Colors.treatment === "on" && ( {(fields, { add, remove }) => { return ( { add({ color: { ...DEFAULT_TRANSLUCENT_CARD_COLOR } }); }} > {t("bodyshop.actions.add_production_status_color")} ]} >
{fields.length === 0 ? ( ) : ( {fields.map((field, index) => { const productionColor = productionColors[field.name] || {}; const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color); const selectedProductionColorStatuses = productionColors .map((item) => item?.status) .filter(Boolean); const productionColorStatusOptions = [ ...new Set([productionColor.status, ...availableProductionStatuses]) ] .filter(Boolean) .filter( (status) => status === productionColor.status || !selectedProductionColorStatuses.includes(status) ); return (