feat: ff deal attributes editor

This commit is contained in:
2025-10-31 20:54:04 +04:00
parent ee90ebe0f0
commit 2948189291
12 changed files with 219 additions and 142 deletions

View File

@ -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>

View File

@ -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>
); );
}; };

View 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;

View File

@ -0,0 +1,5 @@
enum Attributes {
WAREHOUSE_SELECT = 1,
}
export default Attributes;

View File

@ -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"}

View File

@ -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}

View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -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}}",
}, },

View File

@ -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;
}; };