feat: ff deal attributes editor
This commit is contained in:
@ -74,7 +74,7 @@ const DealEditorBody: FC<Props> = props => {
|
||||
value={module.key}>
|
||||
<CustomTab
|
||||
key={module.key}
|
||||
module={module}
|
||||
moduleId={module.id}
|
||||
deal={props.value}
|
||||
/>
|
||||
</DealEditorTabPanel>
|
||||
|
||||
@ -1,147 +1,21 @@
|
||||
import React, {
|
||||
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 "@/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";
|
||||
import React, { FC } from "react";
|
||||
import AttributeEditor from "@/components/ui/AttributesEditor/AttributesEditor";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
module: ModuleSchemaOutput;
|
||||
moduleId: number;
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
type AttrInfo = {
|
||||
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]);
|
||||
|
||||
const CustomTab: FC<Props> = props => {
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={"xs"}
|
||||
py={"xs"}
|
||||
px={"md"}>
|
||||
{attributesRows}
|
||||
<Group>
|
||||
<InlineButton onClick={onSubmit}>Сохранить</InlineButton>
|
||||
</Group>
|
||||
</Flex>
|
||||
<AttributeEditor
|
||||
{...props}
|
||||
containerStyle={{
|
||||
paddingBlock: "var(--mantine-spacing-xs)",
|
||||
paddingInline: "var(--mantine-spacing-md)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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 TotalPriceLabel from "@/modules/dealModularEditorTabs/FulfillmentBase/desktop/components/DealInfoView/components/TotalPriceLabel/TotalPriceLabel";
|
||||
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";
|
||||
|
||||
const DealInfoView = () => (
|
||||
<Stack
|
||||
flex={2}
|
||||
gap={"sm"}>
|
||||
<Flex
|
||||
gap={"sm"}
|
||||
direction={"column"}
|
||||
className={styles.container}>
|
||||
<FulfillmentAttributesEditor />
|
||||
</Flex>
|
||||
<Flex
|
||||
gap={"sm"}
|
||||
direction={"column"}
|
||||
|
||||
@ -13,7 +13,7 @@ const DealServicesTable: FC = () => {
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={"sm"}
|
||||
h={"78vh"}>
|
||||
h={"73vh"}>
|
||||
<DealServicesTitle />
|
||||
<ScrollArea
|
||||
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 = {
|
||||
[ModuleNames.FULFILLMENT_BASE]: {
|
||||
info: {
|
||||
id: 1,
|
||||
key: "fulfillment_base",
|
||||
label: "Фулфиллмент",
|
||||
},
|
||||
@ -48,12 +49,13 @@ const MODULES: ModulesType = {
|
||||
},
|
||||
[ModuleNames.CLIENTS]: {
|
||||
info: {
|
||||
id: 2,
|
||||
key: "clients",
|
||||
label: "Клиенты",
|
||||
},
|
||||
tabs: [
|
||||
{
|
||||
label: "Клиенты",
|
||||
label: "Клиент",
|
||||
key: "clients",
|
||||
icon: <IconUsers />,
|
||||
device: "both",
|
||||
|
||||
@ -50,7 +50,7 @@ const kwargs = getArgs();
|
||||
// region constants
|
||||
const HOST = kwargs.host ?? kwargs.h ?? "127.0.0.1";
|
||||
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(
|
||||
__dirname,
|
||||
|
||||
@ -22,6 +22,7 @@ const MODULES: ModulesType = {
|
||||
{{#each modules}}
|
||||
[ModuleNames.{{uppercase this.key}}]: {
|
||||
info: {
|
||||
id: {{this.id}},
|
||||
key: "{{this.key}}",
|
||||
label: "{{this.label}}",
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ export type ModuleTab = {
|
||||
|
||||
export type Module = {
|
||||
info: {
|
||||
id: number;
|
||||
label: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user