feat: ff deal attributes editor
This commit is contained in:
@ -74,7 +74,7 @@ const DealEditorBody: FC<Props> = props => {
|
|||||||
value={module.key}>
|
value={module.key}>
|
||||||
<CustomTab
|
<CustomTab
|
||||||
key={module.key}
|
key={module.key}
|
||||||
module={module}
|
moduleId={module.id}
|
||||||
deal={props.value}
|
deal={props.value}
|
||||||
/>
|
/>
|
||||||
</DealEditorTabPanel>
|
</DealEditorTabPanel>
|
||||||
|
|||||||
@ -1,147 +1,21 @@
|
|||||||
import React, {
|
import React, { FC } from "react";
|
||||||
FC,
|
import AttributeEditor from "@/components/ui/AttributesEditor/AttributesEditor";
|
||||||
ReactNode,
|
import { DealSchema } from "@/lib/client";
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Flex, Group } from "@mantine/core";
|
|
||||||
import AttributeValueInput from "@/app/deals/drawers/DealEditorDrawer/components/AttributeValueInput";
|
|
||||||
import useDealAttributeValuesActions from "@/app/deals/drawers/DealEditorDrawer/hooks/useDealAttributeValuesActions";
|
|
||||||
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
import {
|
|
||||||
DealModuleAttributeSchema,
|
|
||||||
DealSchema,
|
|
||||||
ModuleSchemaOutput,
|
|
||||||
UpdateDealModuleAttributeSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
module: ModuleSchemaOutput;
|
moduleId: number;
|
||||||
deal: DealSchema;
|
deal: DealSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AttrInfo = {
|
const CustomTab: FC<Props> = props => {
|
||||||
value?: any;
|
|
||||||
isApplicableToGroup: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CustomTab: FC<Props> = ({ module, deal }) => {
|
|
||||||
const { dealAttributes, updateAttributeValues } =
|
|
||||||
useDealAttributeValuesActions({
|
|
||||||
moduleId: module.id,
|
|
||||||
dealId: deal.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [attributeValuesMap, setAttributeValuesMap] = useState<
|
|
||||||
Map<number, AttrInfo | null>
|
|
||||||
>(new Map());
|
|
||||||
const [attributeErrorsMap, setAttributeErrorsMap] = useState<
|
|
||||||
Map<number, string>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const values = new Map<number, AttrInfo | null>();
|
|
||||||
for (const dealAttr of dealAttributes) {
|
|
||||||
values.set(dealAttr.attributeId, {
|
|
||||||
...dealAttr,
|
|
||||||
value: dealAttr.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setAttributeValuesMap(values);
|
|
||||||
}, [dealAttributes]);
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
let isErrorFound = false;
|
|
||||||
for (const attr of dealAttributes) {
|
|
||||||
const value = attributeValuesMap.get(attr.attributeId);
|
|
||||||
if (!attr.isNullable && (value === null || value === undefined)) {
|
|
||||||
attributeErrorsMap.set(attr.attributeId, "Обязательное поле");
|
|
||||||
isErrorFound = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
|
||||||
if (isErrorFound) return;
|
|
||||||
|
|
||||||
const attributeValues: UpdateDealModuleAttributeSchema[] =
|
|
||||||
attributeValuesMap
|
|
||||||
.entries()
|
|
||||||
.map(
|
|
||||||
([attributeId, info]) =>
|
|
||||||
({
|
|
||||||
attributeId,
|
|
||||||
...info,
|
|
||||||
}) as UpdateDealModuleAttributeSchema
|
|
||||||
)
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
updateAttributeValues(attributeValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAttributeElement = useCallback(
|
|
||||||
(attribute: DealModuleAttributeSchema) => (
|
|
||||||
<AttributeValueInput
|
|
||||||
key={attribute.attributeId}
|
|
||||||
attrInfo={attribute}
|
|
||||||
value={attributeValuesMap.get(attribute.attributeId)?.value}
|
|
||||||
onChange={value => {
|
|
||||||
attributeValuesMap.set(attribute.attributeId, {
|
|
||||||
...attribute,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
setAttributeValuesMap(new Map(attributeValuesMap));
|
|
||||||
attributeErrorsMap.delete(attribute.attributeId);
|
|
||||||
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
|
||||||
}}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
error={attributeErrorsMap.get(attribute.attributeId)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[attributeValuesMap, attributeErrorsMap]
|
|
||||||
);
|
|
||||||
|
|
||||||
const attributesRows = useMemo(() => {
|
|
||||||
if (!dealAttributes) return [];
|
|
||||||
const boolAttributes = dealAttributes.filter(
|
|
||||||
a => a.type.type === "bool"
|
|
||||||
);
|
|
||||||
const otherAttributes = dealAttributes.filter(
|
|
||||||
a => a.type.type !== "bool"
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows: ReactNode[] = [];
|
|
||||||
for (let i = 0; i < otherAttributes.length; i += 2) {
|
|
||||||
const rightIdx = i + 1;
|
|
||||||
|
|
||||||
rows.push(
|
|
||||||
<FormFlexRow key={`row${i}`}>
|
|
||||||
{getAttributeElement(otherAttributes[i])}
|
|
||||||
{rightIdx < otherAttributes.length &&
|
|
||||||
getAttributeElement(otherAttributes[rightIdx])}
|
|
||||||
</FormFlexRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const attr of boolAttributes) {
|
|
||||||
rows.push(getAttributeElement(attr));
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}, [dealAttributes, getAttributeElement]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<AttributeEditor
|
||||||
direction={"column"}
|
{...props}
|
||||||
gap={"xs"}
|
containerStyle={{
|
||||||
py={"xs"}
|
paddingBlock: "var(--mantine-spacing-xs)",
|
||||||
px={"md"}>
|
paddingInline: "var(--mantine-spacing-md)",
|
||||||
{attributesRows}
|
}}
|
||||||
<Group>
|
/>
|
||||||
<InlineButton onClick={onSubmit}>Сохранить</InlineButton>
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
169
src/components/ui/AttributesEditor/AttributesEditor.tsx
Normal file
169
src/components/ui/AttributesEditor/AttributesEditor.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Flex, Group } from "@mantine/core";
|
||||||
|
import AttributeValueInput from "@/app/deals/drawers/DealEditorDrawer/components/AttributeValueInput";
|
||||||
|
import useDealAttributeValuesActions from "@/components/ui/AttributesEditor/useDealAttributeValuesActions";
|
||||||
|
import FormFlexRow from "@/components/ui/FormFlexRow/FormFlexRow";
|
||||||
|
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||||
|
import {
|
||||||
|
DealModuleAttributeSchema,
|
||||||
|
DealSchema,
|
||||||
|
UpdateDealModuleAttributeSchema,
|
||||||
|
} from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
moduleId: number;
|
||||||
|
deal: DealSchema;
|
||||||
|
containerStyle?: CSSProperties;
|
||||||
|
buttonContainerStyle?: CSSProperties;
|
||||||
|
buttonStyle?: CSSProperties;
|
||||||
|
attributesInTwoColumns?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttrInfo = {
|
||||||
|
value?: any;
|
||||||
|
isApplicableToGroup: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AttributeEditor: FC<Props> = ({
|
||||||
|
moduleId,
|
||||||
|
deal,
|
||||||
|
containerStyle,
|
||||||
|
buttonContainerStyle,
|
||||||
|
buttonStyle,
|
||||||
|
attributesInTwoColumns = true,
|
||||||
|
}) => {
|
||||||
|
const { dealAttributes, updateAttributeValues } =
|
||||||
|
useDealAttributeValuesActions({
|
||||||
|
moduleId,
|
||||||
|
dealId: deal.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [attributeValuesMap, setAttributeValuesMap] = useState<
|
||||||
|
Map<number, AttrInfo | null>
|
||||||
|
>(new Map());
|
||||||
|
const [attributeErrorsMap, setAttributeErrorsMap] = useState<
|
||||||
|
Map<number, string>
|
||||||
|
>(new Map());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const values = new Map<number, AttrInfo | null>();
|
||||||
|
for (const dealAttr of dealAttributes) {
|
||||||
|
values.set(dealAttr.attributeId, {
|
||||||
|
...dealAttr,
|
||||||
|
value: dealAttr.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setAttributeValuesMap(values);
|
||||||
|
}, [dealAttributes]);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
let isErrorFound = false;
|
||||||
|
for (const attr of dealAttributes) {
|
||||||
|
const value = attributeValuesMap.get(attr.attributeId);
|
||||||
|
if (!attr.isNullable && (value === null || value === undefined)) {
|
||||||
|
attributeErrorsMap.set(attr.attributeId, "Обязательное поле");
|
||||||
|
isErrorFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
||||||
|
if (isErrorFound) return;
|
||||||
|
|
||||||
|
const attributeValues: UpdateDealModuleAttributeSchema[] =
|
||||||
|
attributeValuesMap
|
||||||
|
.entries()
|
||||||
|
.map(
|
||||||
|
([attributeId, info]) =>
|
||||||
|
({
|
||||||
|
attributeId,
|
||||||
|
...info,
|
||||||
|
}) as UpdateDealModuleAttributeSchema
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
updateAttributeValues(attributeValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttributeElement = useCallback(
|
||||||
|
(attribute: DealModuleAttributeSchema) => (
|
||||||
|
<AttributeValueInput
|
||||||
|
key={attribute.attributeId}
|
||||||
|
attrInfo={attribute}
|
||||||
|
value={attributeValuesMap.get(attribute.attributeId)?.value}
|
||||||
|
onChange={value => {
|
||||||
|
attributeValuesMap.set(attribute.attributeId, {
|
||||||
|
...attribute,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
setAttributeValuesMap(new Map(attributeValuesMap));
|
||||||
|
attributeErrorsMap.delete(attribute.attributeId);
|
||||||
|
setAttributeErrorsMap(new Map(attributeErrorsMap));
|
||||||
|
}}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={attributeErrorsMap.get(attribute.attributeId)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[attributeValuesMap, attributeErrorsMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributesRows = useMemo(() => {
|
||||||
|
if (!dealAttributes) return [];
|
||||||
|
const boolAttributes = dealAttributes.filter(
|
||||||
|
a => a.type.type === "bool"
|
||||||
|
);
|
||||||
|
const otherAttributes = dealAttributes.filter(
|
||||||
|
a => a.type.type !== "bool"
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (attributesInTwoColumns) {
|
||||||
|
for (let i = 0; i < otherAttributes.length; i += 2) {
|
||||||
|
const rightIdx = i + 1;
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<FormFlexRow key={`row${i}`}>
|
||||||
|
{getAttributeElement(otherAttributes[i])}
|
||||||
|
{rightIdx < otherAttributes.length &&
|
||||||
|
getAttributeElement(otherAttributes[rightIdx])}
|
||||||
|
</FormFlexRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const attr of otherAttributes) {
|
||||||
|
rows.push(getAttributeElement(attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attr of boolAttributes) {
|
||||||
|
rows.push(getAttributeElement(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}, [dealAttributes, getAttributeElement]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction={"column"}
|
||||||
|
gap={"xs"}
|
||||||
|
style={containerStyle}>
|
||||||
|
{attributesRows}
|
||||||
|
<Group style={buttonContainerStyle}>
|
||||||
|
<InlineButton
|
||||||
|
style={buttonStyle}
|
||||||
|
onClick={onSubmit}>
|
||||||
|
Сохранить
|
||||||
|
</InlineButton>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AttributeEditor;
|
||||||
5
src/modules/attributes.tsx
Normal file
5
src/modules/attributes.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
enum Attributes {
|
||||||
|
WAREHOUSE_SELECT = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Attributes;
|
||||||
@ -2,12 +2,19 @@ import { Flex, Stack } from "@mantine/core";
|
|||||||
import ProductsActions from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions";
|
import ProductsActions from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/ProductsActions/ProductsActions";
|
||||||
import TotalPriceLabel from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/TotalPriceLabel/TotalPriceLabel";
|
import TotalPriceLabel from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/TotalPriceLabel/TotalPriceLabel";
|
||||||
import DealServicesTable from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealServicesTable/DealServicesTable";
|
import DealServicesTable from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealServicesTable/DealServicesTable";
|
||||||
|
import FulfillmentAttributesEditor from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/FulfillmentAttributesEditor/FulfillmentAttributesEditor";
|
||||||
import styles from "@/modules/dealModularEditorTabs/FulfillmentBase/FulfillmentBase.module.css";
|
import styles from "@/modules/dealModularEditorTabs/FulfillmentBase/FulfillmentBase.module.css";
|
||||||
|
|
||||||
const DealInfoView = () => (
|
const DealInfoView = () => (
|
||||||
<Stack
|
<Stack
|
||||||
flex={2}
|
flex={2}
|
||||||
gap={"sm"}>
|
gap={"sm"}>
|
||||||
|
<Flex
|
||||||
|
gap={"sm"}
|
||||||
|
direction={"column"}
|
||||||
|
className={styles.container}>
|
||||||
|
<FulfillmentAttributesEditor />
|
||||||
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
gap={"sm"}
|
gap={"sm"}
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const DealServicesTable: FC = () => {
|
|||||||
<Flex
|
<Flex
|
||||||
direction={"column"}
|
direction={"column"}
|
||||||
gap={"sm"}
|
gap={"sm"}
|
||||||
h={"78vh"}>
|
h={"73vh"}>
|
||||||
<DealServicesTitle />
|
<DealServicesTitle />
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
flex={1}
|
flex={1}
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
import AttributesEditor from "@/components/ui/AttributesEditor/AttributesEditor";
|
||||||
|
import { useFulfillmentBaseContext } from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/contexts/FulfillmentBaseContext";
|
||||||
|
import MODULES, { ModuleNames } from "@/modules/modules";
|
||||||
|
|
||||||
|
const FulfillmentAttributesEditor = () => {
|
||||||
|
const { deal } = useFulfillmentBaseContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AttributesEditor
|
||||||
|
moduleId={MODULES[ModuleNames.FULFILLMENT_BASE].info.id}
|
||||||
|
deal={deal}
|
||||||
|
attributesInTwoColumns
|
||||||
|
buttonStyle={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FulfillmentAttributesEditor;
|
||||||
@ -19,6 +19,7 @@ export enum ModuleNames {
|
|||||||
const MODULES: ModulesType = {
|
const MODULES: ModulesType = {
|
||||||
[ModuleNames.FULFILLMENT_BASE]: {
|
[ModuleNames.FULFILLMENT_BASE]: {
|
||||||
info: {
|
info: {
|
||||||
|
id: 1,
|
||||||
key: "fulfillment_base",
|
key: "fulfillment_base",
|
||||||
label: "Фулфиллмент",
|
label: "Фулфиллмент",
|
||||||
},
|
},
|
||||||
@ -48,12 +49,13 @@ const MODULES: ModulesType = {
|
|||||||
},
|
},
|
||||||
[ModuleNames.CLIENTS]: {
|
[ModuleNames.CLIENTS]: {
|
||||||
info: {
|
info: {
|
||||||
|
id: 2,
|
||||||
key: "clients",
|
key: "clients",
|
||||||
label: "Клиенты",
|
label: "Клиенты",
|
||||||
},
|
},
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
label: "Клиенты",
|
label: "Клиент",
|
||||||
key: "clients",
|
key: "clients",
|
||||||
icon: <IconUsers />,
|
icon: <IconUsers />,
|
||||||
device: "both",
|
device: "both",
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const kwargs = getArgs();
|
|||||||
// region constants
|
// region constants
|
||||||
const HOST = kwargs.host ?? kwargs.h ?? "127.0.0.1";
|
const HOST = kwargs.host ?? kwargs.h ?? "127.0.0.1";
|
||||||
const PORT = kwargs.port ?? kwargs.p ?? "8000";
|
const PORT = kwargs.port ?? kwargs.p ?? "8000";
|
||||||
const ENDPOINT = `http://${HOST}:${PORT}/api/module/built-in/`;
|
const ENDPOINT = `http://${HOST}:${PORT}/api/crm/v1/module/`;
|
||||||
|
|
||||||
const TEMPLATE_PATH = path.join(
|
const TEMPLATE_PATH = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const MODULES: ModulesType = {
|
|||||||
{{#each modules}}
|
{{#each modules}}
|
||||||
[ModuleNames.{{uppercase this.key}}]: {
|
[ModuleNames.{{uppercase this.key}}]: {
|
||||||
info: {
|
info: {
|
||||||
|
id: {{this.id}},
|
||||||
key: "{{this.key}}",
|
key: "{{this.key}}",
|
||||||
label: "{{this.label}}",
|
label: "{{this.label}}",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export type ModuleTab = {
|
|||||||
|
|
||||||
export type Module = {
|
export type Module = {
|
||||||
info: {
|
info: {
|
||||||
|
id: number;
|
||||||
label: string;
|
label: string;
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user