Files
bodyshop/client/src/components/shop-info/shop-info.rostatus.component.jsx

530 lines
18 KiB
JavaScript

import { CloseOutlined, DeleteFilled } 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 { 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 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 "@splitsoftware/splitio-react";
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: 4px;
touch-action: none;
}
.job-statuses-source-tag-wrapper .ant-select-selection-item {
max-width: 100%;
cursor: grab;
margin-inline-end: 0;
user-select: none;
}
.job-statuses-source-tag-wrapper .ant-select-selection-item-content {
overflow: hidden;
text-overflow: ellipsis;
}
.job-statuses-source-tag-wrapper .ant-select-selection-item:active {
cursor: grabbing;
}
.job-statuses-source-tag-wrapper .ant-select-selection-item-remove {
flex: none;
}
.job-statuses-source-tag-wrapper--dragging {
opacity: 0.55;
}
`;
const normalizeStatuses = (statuses) => [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))];
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" : ""}`}
style={{ transform: CSS.Transform.toString(transform), transition }}
onPointerDownCapture={(event) => {
if (event.target.closest(".ant-tag-close-icon")) {
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();
}}
title={labelText}
>
<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 }) => {
const statuses = normalizeStatuses(value);
const tagSensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 6
}
})
);
const handleStatusesChange = (nextValues) => {
onChange?.(normalizeStatuses(nextValues));
};
const handleStatusSortEnd = ({ active, over }) => {
if (!over || active.id === over.id) return;
const oldIndex = statuses.indexOf(active.id);
const newIndex = statuses.indexOf(over.id);
if (oldIndex < 0 || 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} />;
};
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleStatusSortEnd} sensors={tagSensors}>
<SortableContext items={statuses} strategy={rectSortingStrategy}>
<Select
className="job-statuses-source-select"
mode="tags"
onChange={handleStatusesChange}
tagRender={renderStatusTag}
value={statuses}
/>
</SortableContext>
</DndContext>
);
};
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 (
<SelectorDiv id="jobstatus">
<LayoutFormRow grow header={t("bodyshop.labels.job_status_options")}>
<div>
<Form.Item
name={["md_ro_statuses", "statuses"]}
label={t("bodyshop.labels.alljobstatuses")}
rules={[
{
validator: async (_, value) => {
const populatedStatuses = normalizeStatuses(value);
if (populatedStatuses.length === 0) {
return Promise.reject(new Error(t("general.validation.required")));
}
if (populatedStatuses.length !== (value || []).filter(Boolean).length) {
return Promise.reject(new Error(t("bodyshop.errors.duplicate_job_status")));
}
}
}
]}
>
<SortableStatusesSelect />
</Form.Item>
<Form.Item
name={["md_ro_statuses", "active_statuses"]}
label={t("bodyshop.fields.statuses.active_statuses")}
rules={[
{
required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
>
<Select mode="multiple" options={statusSelectOptions} />
</Form.Item>
<Form.Item
name={["md_ro_statuses", "pre_production_statuses"]}
label={t("bodyshop.fields.statuses.pre_production_statuses")}
rules={[
{
required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
>
<Select mode="multiple" options={statusSelectOptions} />
</Form.Item>
<Form.Item
name={["md_ro_statuses", "production_statuses"]}
label={t("bodyshop.fields.statuses.production_statuses")}
rules={[
{
required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
>
<Select mode="multiple" options={statusSelectOptions} />
</Form.Item>
<Form.Item
name={["md_ro_statuses", "post_production_statuses"]}
label={t("bodyshop.fields.statuses.post_production_statuses")}
rules={[
{
required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
>
<Select mode="multiple" options={statusSelectOptions} />
</Form.Item>
<Form.Item
name={["md_ro_statuses", "ready_statuses"]}
label={t("bodyshop.fields.statuses.ready_statuses")}
rules={[
{
//required: true,
//message: t("general.validation.required"),
type: "array"
}
]}
>
<Select 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"
}
]}
>
<Select mode="multiple" options={statusSelectOptions} />
</Form.Item>
</div>
</LayoutFormRow>
<LayoutFormRow grow header={t("general.actions.defaults")}>
<Form.Item
label={t("bodyshop.fields.statuses.default_scheduled")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_scheduled"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_arrived")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_arrived"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_exported")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_exported"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_imported")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_imported"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_invoiced")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_invoiced"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_completed")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_completed"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_delivered")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_delivered"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.statuses.default_void")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_ro_statuses", "default_void"]}
>
<Select options={statusSelectOptions} />
</Form.Item>
</LayoutFormRow>
{Production_List_Status_Colors.treatment === "on" && (
<Form.List name={["md_ro_statuses", "production_colors"]}>
{(fields, { add, remove }) => {
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>
{fields.length === 0 ? (
<ConfigListEmptyState actionLabel={t("bodyshop.actions.add_production_status_color")} />
) : (
<Space size="large" wrap align="start">
{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 (
<LayoutFormRow
key={field.key}
noDivider
title={
<Form.Item
noStyle
key={`${index}status`}
name={[field.name, "status"]}
rules={[
{
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 }}
>
<div>
<Form.Item
key={`${index}color`}
name={[field.name, "color"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<ColorPicker />
</Form.Item>
</div>
</LayoutFormRow>
);
})}
</Space>
)}
</div>
</LayoutFormRow>
);
}}
</Form.List>
)}
</SelectorDiv>
);
}
export const ColorPicker = ({ value, onChange, ...restProps }) => {
const handleChange = (color) => {
if (onChange) onChange(color.rgb);
};
return <ChromePicker {...restProps} color={value} onChangeComplete={handleChange} />;
};