Загрузка данных


----Form.tsx
import { FieldValues, FormProvider, useForm } from "react-hook-form";
import { Button } from "@shared/ui";
import { FormAction, FormConfig } from "@shared/types/form";
import { Box } from "@mui/material";
import styles from "./Form.module.scss";
import { FormGroup } from "@shared/ui/components/form/form-group/FormGroup";
import { SimpleValueTag } from "../../../../../examples/object-details/SimpleValueTag";
import { SimpleValue } from "@lib/models/SimpleValue";
import {
  AttributeDataDto,
  AttributeValuesModifyRequest,
  ObjectVedAddRequest,
} from "@gnr/api";
import {
  NestedAttributeData,
  RootAttributeData,
  SimpleAttributeData,
} from "@lib/dtos/AttributeDataDto";
import { AttributeDataTag } from "@lib/tags/AttributeDataTag";
import { useEffect, useMemo, useState } from "react";
import { sortByFieldOrder } from "@shared/utils/sortByFieldOrder";
import { useRootAccordion } from "@shared/hooks";
import {
  CommonFormObject,
  CommonFormValuesType,
} from "../common-form-object/CommonFormObject";
import { useObjectGetById } from "@api";
import { collectUsedRoots, UsedRoot } from "@shared/utils/attributeUseValue";

export const Form = <T,>({
  fields,
  actions,
  loading = false,
  objectId,
  expandAll,
  setExpandAll,
  type,
  sectionRefs,
  parentForm,
}: FormConfig<T>) => {
  // eslint-disable-next-line no-undef
  const currentForm = useForm({
    mode: "onBlur",
    shouldFocusError: true,
  });
  const form = parentForm ?? currentForm;
  const {
    handleSubmit,
    formState: { isSubmitting, dirtyFields },
    reset,
  } = form;

  const { data: objData } = useObjectGetById(objectId, type === "update");

  const [usedRoots, setUsedRoots] = useState<UsedRoot[]>(() =>
    type === "filter" || !fields?.length
      ? []
      : collectUsedRoots(fields, type, objectId),
  );

  useEffect(() => {
    if (type === "filter" || !fields?.length) {
      setUsedRoots([]);
      return;
    }
    setUsedRoots(collectUsedRoots(fields, type, objectId));
  }, [fields, type, objectId]);

  const [commonValues, setCommonValues] = useState<CommonFormValuesType>({
    name: objData?.name ?? "",
    imagePath: objData?.imagePath ?? "",
    zone: null,
    hideFilled: false,
  });

  useEffect(() => {
    setCommonValues({
      name: objData?.name ?? "",
      imagePath: objData?.imagePath ?? "",
      zone: null,
      hideFilled: false,
    });
  }, [objData]);

  const handleActionClick = (action: FormAction) => {
    if (action.type === "submit") {
      handleSubmit((data) => submitHandler(data, action))();
    } else if (action.type === "reset") {
      reset({});

      action.onClick();
    }
  };

  const submitHandler = (data: FieldValues, action: FormAction) => {
    const changedFieldsIds = Object.keys(dirtyFields);
    const transformedData = transformFormData(
      fields,
      data,
      changedFieldsIds,
      type,
      objectId,
    );

    const attrVals = transformedData.attributeValues ?? [];
    action.onClick([...attrVals, ...usedRoots], commonValues, data);
  };

  const sortedFields = useMemo(() => {
    return fields?.slice()?.sort(sortByFieldOrder) || [];
  }, [fields]);

  const rootAccordionIds = useMemo(() => {
    return sortedFields?.map((item) => item.id) || [];
  }, [sortedFields]);

  const { handleAccordionChange, isExpanded } = useRootAccordion({
    rootAccordionIds,
    expandAll,
    setExpandAll,
  });

  return (
    <Box className={styles.formWrap}>
      <CommonFormObject
        onValuesChange={setCommonValues}
        commonValues={commonValues}
        type={type}
      />
      <FormProvider {...form}>
        <form style={{ width: "100%" }}>
          {sortedFields?.map((fieldGroup) => (
            <div
              ref={(el: HTMLDivElement | null) => {
                sectionRefs.current[fieldGroup.id] = el;
              }}
              key={fieldGroup.name}
            >
              <FormGroup
                fieldGroup={fieldGroup}
                objectId={objectId}
                expanded={isExpanded(fieldGroup.id)}
                onExpandChange={handleAccordionChange(fieldGroup.id)}
                setUsedRoots={setUsedRoots}
                usedRoots={usedRoots}
                zone={commonValues.zone}
                type={type}
                hideFilled={commonValues.hideFilled}
              />
            </div>
          ))}

          <Box className={styles.buttonsContainer}>
            {actions.map((action, index) => (
              <Button
                key={index}
                onClick={(e) => {
                  e.preventDefault();
                  handleActionClick(action);
                }}
                color={action.color ?? "primary"}
                disabled={
                  loading ||
                  isSubmitting ||
                  (type !== "filter" && !commonValues.name)
                }
                label={loading || isSubmitting ? "Загрузка..." : action.label}
              />
            ))}
          </Box>
        </form>
      </FormProvider>
    </Box>
  );
};

const getDefaultFormValue = (simple: SimpleAttributeData): unknown => {
  const value = simple.value;
  if (!value) return undefined;

  return SimpleValueTag.matchWithArg<unknown>({
    boolean: (v) => v.value,
    string: (v) => v.value ?? "",
    number: (v) => v.value,
    nullableBool: (v) => v.value,
    date: (v) => v.value,
    htmlText: (v) => v.value ?? "",
    file: (v) => v.value,
    image: (v) => v.value,
    money: (v) => v.value,
    singleChoiceListItem: (v) => v.value.find((item) => item.isActive),
    pluralChoiceListItem: (v) => v.value.filter((item) => item.isActive),
    webLink: (v) => ({ url: v.url, name: v.name || "" }),
    categoryLink: (v) => v.value,
    linkedObjectList: (v) => v.value,
    objectAttributeLink: (v) => v,
  })(value);
};

const collectValues = (
  groups: readonly AttributeDataDto[],
  formData: FieldValues,
  changedFieldsIds: string[],
  type: "update" | "create" | "filter",
  objectId: number,
) => {
  const attributeValues: AttributeValuesModifyRequest[] = [];

  groups.forEach((group) => {
    AttributeDataTag.match({
      root: () => {
        const root = group as RootAttributeData;
        attributeValues.push(
          ...collectValues(
            root.children as readonly AttributeDataDto[],
            formData,
            changedFieldsIds,
            type,
            objectId,
          ),
        );
      },
      simple: () => {
        if (
          (type === "update" ||
            ((type === "create" || type === "filter") && !objectId)) &&
          !changedFieldsIds.includes(group.id.toString())
        )
          return;

        const simple = group as SimpleAttributeData;
        const formValue = formData[group.id];
        const valueToParse =
          formValue !== undefined
            ? formValue
            : type === "create" && objectId
              ? getDefaultFormValue(simple)
              : formValue;
        const parsedValue = parseFormValue(valueToParse, simple);
        attributeValues.push({
          attributeId: group.id,
          showInTable: group.showInTable,
          useForComparison: group.useForComparison,
          value: SimpleValue.toDto(parsedValue),
        });
      },
      nested: () => {
        const nested = group as NestedAttributeData;
        attributeValues.push(
          ...collectValues(
            nested.children as readonly AttributeDataDto[],
            formData,
            changedFieldsIds,
            type,
            objectId,
          ),
        );
      },
    })(group.tag);
  });
  return attributeValues;
};

const transformFormData = (
  fields: readonly AttributeDataDto[],
  formData: FieldValues,
  changedFieldsIds: string[],
  type: "update" | "create" | "filter",
  objectId: number,
): Pick<ObjectVedAddRequest, "attributeValues"> => {
  const attributeValues: AttributeValuesModifyRequest[] = collectValues(
    fields || [],
    formData,
    changedFieldsIds,
    type,
    objectId,
  );

  return { attributeValues };
};

const parseFormValue = (value: any, group: SimpleAttributeData) => {
  const simple = group.value;

  const parseParams = SimpleValueTag.matchWithArg({
    boolean: () => ({ tag: "native", value: value ?? null }),
    string: () => ({ tag: "native", value: value ? String(value) : "" }),
    number: () => ({ tag: "native", value: value ? String(value) : "0" }),
    nullableBool: () => ({ tag: "native", value: value ?? null }),
    date: () => ({ tag: "native", value: value ? String(value) : null }),
    htmlText: () => ({ tag: "native", value: value ? String(value) : "" }),
    file: () => ({
      tag: "native",
      value: value ? String(value) : "no-file.jpg",
    }),
    image: () => ({
      tag: "native",
      value: value ? String(value) : "no-image.jpg",
    }),
    money: () => ({ tag: "native", value: value ? String(value) : "0" }),
    singleChoiceListItem: () => ({ tag: "id", ...value }),
    webLink: () => ({
      tag: "webLink",
      url: value.url || "https://no-url.com",
      name: value.name,
    }),
    pluralChoiceListItem: () => ({
      tag: "ids",
      ids: value?.map((f: { id: number }) => f.id) ?? [],
    }),
    categoryLink: () => ({
      tag: "attributeAndCategory",
      attributeId: group.id,
      categoryId: value.categoryId ?? null,
    }),
    linkedObjectList: () => ({
      tag: "attributeAndObjectList",
      items: Array.isArray(value) ? value : [value],
    }),

    objectAttributeLink: () => ({
      tag: "objectAttributeLink",
      attributeId: value.attributeId,
      linkObjectId: value.linkObjectId,
      linkAttributeId: value.linkAttributeId,
      link: {
        tag: value.link.tag,
      },
    }),
  })(simple);

  // @ts-ignore
  return SimpleValueTag.parse(group.value.tag)(parseParams);
};

----FormGroup.tsx
import React, { FC, useMemo, useCallback, useState } from "react";
import {
  Accordion,
  AccordionDetails,
  AccordionSummary,
  Box,
  Typography,
} from "@mui/material";
import { Switch } from "@components";
import styles from "./FormGroup.module.scss";
import { FormField } from "@shared/ui/components/form/form-field/FormField";
import { ExpandMore } from "@mui/icons-material";
import { AttributeDataTag } from "../../../../../examples/object-details/AttributeDataTag";
import { sortByFieldOrder } from "@shared/utils/sortByFieldOrder";
import { AttributeDataDto } from "@gnr/api";
import {
  NestedAttributeData,
  RootAttributeData,
} from "@lib/dtos/AttributeDataDto.ts";
import { SimpleValueTag } from "@lib/tags/SimpleValueTag";
import {
  isUsedRootChecked,
  toggleUsedRoot,
  UsedRoot,
} from "@shared/utils/attributeUseValue";

type FormGroupType = "filter" | "update" | "create";

type Props = {
  readonly fieldGroup: AttributeDataDto;
  readonly objectId: number;
  readonly expanded?: boolean;
  readonly onExpandChange?: (
    event: React.SyntheticEvent,
    isExpanded: boolean,
  ) => void;
  readonly setUsedRoots?: React.Dispatch<React.SetStateAction<UsedRoot[]>>;
  readonly usedRoots?: UsedRoot[];
  readonly zone: number | null;
  readonly type: FormGroupType;
  readonly hideFilled?: boolean;
};

export const FormGroup: React.FC<Props> = ({
  fieldGroup,
  objectId,
  expanded,
  onExpandChange,
  setUsedRoots,
  usedRoots,
  zone,
  type,
  hideFilled,
}) => {
  const group = useMemo(
    () =>
      groupGeneration({
        objectId,
        expanded,
        onExpandChange,
        setUsedRoots,
        usedRoots,
        zone,
        type,
        fieldGroup,
        hideFilled,
      }),
    [
      expanded,
      fieldGroup,
      objectId,
      onExpandChange,
      setUsedRoots,
      usedRoots,
      type,
      zone,
      hideFilled,
    ],
  );

  const hasVisibleContent = useMemo(() => {
    const checkVisibleChildren = (group: AttributeDataDto): boolean => {
      if (
        zone !== null &&
        group.tag === "simple" &&
        group.responsibilityZone !== zone
      ) {
        return false;
      }

      if (group.tag === "simple" && hideFilled) {
        return AttributeDataTag.matchWithArg({
          root: () => true,
          nested: () => true,
          simple: (v) => {
            return SimpleValueTag.matchWithArg({
              boolean: (v) => !v.value,
              string: (v) => !v.value,
              number: (v) => !v.value,
              nullableBool: (v) => !v.value,
              file: (v) => !v.value,
              webLink: (v) => !v.url || !v.name,
              image: (v) => !v.value,
              money: (v) => !v.value,
              date: (v) => !v.value,
              htmlText: (v) => !v.value,
              categoryLink: (v) => !v.value,
              objectAttributeLink: (v) => !v.linkPath,
              pluralChoiceListItem: (v) =>
                v.value.every((val) => !val.isActive),
              singleChoiceListItem: (v) =>
                !v.value?.some((val) => val.isActive),
              linkedObjectList: (v) => !v.value,
            })(v.value);
          },
        })(group);
      }

      if (group.tag === "root" || group.tag === "nested") {
        return (
          // @ts-ignore
          group.children?.some((child: AttributeDataDto) =>
            checkVisibleChildren(child),
          ) || false
        );
      }

      return true;
    };

    return checkVisibleChildren(fieldGroup);
  }, [fieldGroup, zone, hideFilled]);

  if (!hasVisibleContent) {
    return null;
  }

  return (
    <Box className={styles.formGroup}>
      <Box className={styles.formFields}>{group}</Box>
    </Box>
  );
};

function groupGeneration({
  objectId,
  expanded,
  onExpandChange,
  setUsedRoots,
  usedRoots,
  zone,
  type,
  fieldGroup,
  hideFilled,
}: Props) {
  return AttributeDataTag.matchWithArg({
    root: (v) => (
      <RootComponent
        key={v.name}
        children={v.children}
        name={v.name}
        objectId={objectId}
        expanded={expanded}
        onExpandChange={onExpandChange}
        v={v}
        setUsedRoots={setUsedRoots}
        usedRoots={usedRoots}
        zone={zone}
        type={type}
        hideFilled={hideFilled}
      />
    ),
    simple: (v) => {
      return (
        <FormField
          key={v.name}
          value={v.value}
          name={v.name}
          id={v.id}
          objectId={objectId}
          targetCategoryId={v.targetCategoryId}
          type={type}
          required={type !== "filter" ? (v.required ?? false) : false}
        />
      );
    },
    nested: (v) => (
      <NestedComponent
        name={v.name}
        children={v.children}
        objectId={objectId}
        zone={zone}
        type={type}
        v={v}
        setUsedRoots={setUsedRoots}
        usedRoots={usedRoots}
        hideFilled={hideFilled}
      />
    ),
  })(fieldGroup);
}

type RootProps = {
  readonly name: string;
  readonly children: AttributeDataDto[];
  readonly objectId: number;
  readonly expanded?: boolean;
  readonly onExpandChange?: (
    event: React.SyntheticEvent,
    isExpanded: boolean,
  ) => void;
  readonly v: RootAttributeData;
  readonly setUsedRoots?: React.Dispatch<React.SetStateAction<UsedRoot[]>>;
  readonly usedRoots?: UsedRoot[];
  readonly zone: number | null;
  readonly type: FormGroupType;
  readonly hideFilled?: boolean;
};

const RootComponent: FC<RootProps> = ({
  name,
  children,
  objectId,
  expanded,
  onExpandChange,
  v,
  setUsedRoots,
  usedRoots,
  zone,
  type,
  hideFilled,
}) => {
  const sortedChildren = useMemo(() => {
    const filtered =
      zone !== null
        ? children?.filter((child) => {
            if (child.tag === "simple") {
              return child.responsibilityZone === zone;
            }
            return true;
          }) || []
        : children || [];
    return filtered.slice().sort(sortByFieldOrder);
  }, [children, zone]);

  const isChecked = isUsedRootChecked(usedRoots, v.id);

  const onSwitchUseRoot = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      if (setUsedRoots) {
        setUsedRoots((prev) =>
          toggleUsedRoot(prev, v.id, event.target.checked),
        );
      }
    },
    [setUsedRoots, v.id],
  );

  return (
    <Accordion elevation={0} expanded={expanded} onChange={onExpandChange}>
      <AccordionSummary
        expandIcon={<ExpandMore />}
        className={styles.titleBlock}
      >
        <Box className={styles.titleBlockWithSwitch}>
          <Typography className={styles.title}>{name}</Typography>
          {v.id !== 0 && type !== "filter" && (
            <Switch
              label="Использовать"
              checked={isChecked}
              onChange={onSwitchUseRoot}
              onClick={(e) => e.stopPropagation()}
            />
          )}
        </Box>
      </AccordionSummary>
      <AccordionDetails className={styles.accordionDetail}>
        {expanded && sortedChildren.length > 0
          ? sortedChildren.map((i) => (
              <FormGroup
                key={i.id}
                fieldGroup={i}
                objectId={objectId}
                zone={zone}
                type={type}
                setUsedRoots={setUsedRoots}
                usedRoots={usedRoots}
                hideFilled={hideFilled}
              />
            ))
          : null}
      </AccordionDetails>
    </Accordion>
  );
};

type NestedProps = {
  readonly name: string;
  readonly children: AttributeDataDto[];
  readonly objectId: number;
  readonly zone: number | null;
  readonly type: FormGroupType;
  readonly v: NestedAttributeData;
  readonly setUsedRoots?: React.Dispatch<React.SetStateAction<UsedRoot[]>>;
  readonly usedRoots?: UsedRoot[];
  readonly hideFilled?: boolean;
};

const NestedComponent: FC<NestedProps> = ({
  name,
  children,
  objectId,
  zone,
  type,
  v,
  setUsedRoots,
  usedRoots,
  hideFilled,
}) => {
  const [expanded, setExpanded] = useState(false);

  const sortedChildren = useMemo(() => {
    const filtered =
      zone !== null
        ? children?.filter((child) => {
            if (child.tag === "simple") {
              return child.responsibilityZone === zone;
            }
            return true;
          }) || []
        : children || [];
    return filtered.slice().sort(sortByFieldOrder);
  }, [children, zone]);

  const isChecked = isUsedRootChecked(usedRoots, v.id);

  const onSwitchUseRoot = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      if (setUsedRoots) {
        setUsedRoots((prev) =>
          toggleUsedRoot(prev, v.id, event.target.checked),
        );
      }
    },
    [setUsedRoots, v.id],
  );

  return (
    <Accordion
      className={styles.toggleSection}
      expanded={expanded}
      onChange={(_, isExpanded) => setExpanded(isExpanded)}
    >
      <AccordionSummary expandIcon={<ExpandMore />}>
        <Box className={styles.titleBlockWithSwitch}>
          <Typography className={styles.toggleSectionTitle}>{name}</Typography>
          {v.id !== 0 && type !== "filter" && (
            <Switch
              label="Использовать"
              checked={isChecked}
              onChange={onSwitchUseRoot}
              onClick={(e) => e.stopPropagation()}
            />
          )}
        </Box>
      </AccordionSummary>
      <AccordionDetails className={styles.toggleSectionContent}>
        {expanded && sortedChildren.length > 0
          ? sortedChildren.map((i) => (
              <FormGroup
                key={i.id}
                fieldGroup={i}
                objectId={objectId}
                zone={zone}
                type={type}
                setUsedRoots={setUsedRoots}
                usedRoots={usedRoots}
                hideFilled={hideFilled}
              />
            ))
          : null}
      </AccordionDetails>
    </Accordion>
  );
};

----index.ts
export * from "./RecursionUtils";
export * from "./sort";
export * from "./attributeUseValue";

----CategoryObjectCardInfoModalDetails.tsx
import * as React from "react";
import { SimpleComponent } from "../simple-component/SimpleComponent";
import { AttributeDataDto } from "@lib/dtos/AttributeDataDto";
import { AttributeDataTag } from "@lib/tags/AttributeDataTag";
import {
  Accordion,
  AccordionDetails,
  AccordionSummary,
  Box,
  CircularProgress,
  Typography,
} from "@mui/material";
import styles from "./CategoryObjectCardInfoModalDetails.module.scss";
import { ExpandMore } from "@mui/icons-material";
import clsx from "clsx";
import { useMemo, useState } from "react";
import { sortByFieldOrder } from "@shared/utils/sortByFieldOrder";
import { useRootAccordion } from "@shared/hooks";
import { isAttributeUsed } from "@shared/utils/attributeUseValue";
import { CommonDetails } from "./components/commonDetails/CommonDetails";

type Props = Readonly<{
  readonly sectionRefs: React.RefObject<{
    [key: number]: HTMLDivElement | null;
  }>;
  readonly id: number;
  readonly expandAll: boolean;
  readonly setExpandAll: React.Dispatch<React.SetStateAction<boolean>>;
  dataWithLoading: { data?: readonly AttributeDataDto[]; isLoading?: boolean };
  readonly title: string;
}>;

export const CategoryObjectCardInfoModalDetails: React.FC<Props> = ({
  sectionRefs,
  id,
  expandAll,
  setExpandAll,
  dataWithLoading,
  title,
}) => {
  const { data, isLoading } = dataWithLoading;

  const [responsibilityZone, setResponsibilityZone] = useState<number | null>(
    null,
  );

  const sortedData = useMemo(() => {
    return data?.slice()?.sort(sortByFieldOrder) || [];
  }, [data]);

  const isSwitchActive = (
    item: AttributeDataDto,
    isDirectChildOfRootId0 = false,
  ) => isAttributeUsed(item, isDirectChildOfRootId0) || item.id === 0;

  const rootAccordionIds = useMemo(() => {
    return sortedData?.map((item) => item.id) || [];
  }, [sortedData]);

  const { handleAccordionChange, isExpanded } = useRootAccordion({
    rootAccordionIds,
    expandAll,
    setExpandAll,
  });

  const renderChildren = (
    children: AttributeDataDto[],
    level: number = 1,
    zone: number | null = null,
    isDirectChildOfRootId0 = false,
  ) => {
    const filtered =
      zone !== null
        ? children?.filter((child) => {
            if (child.tag === "simple") {
              return child.responsibilityZone === zone;
            }
            return true;
          }) || []
        : children || [];
    const sortedChildren = filtered.slice().sort(sortByFieldOrder);

    return sortedChildren.map((child) => (
      <Box
        key={child.id}
        sx={{
          flex: child.tag === "nested" ? "1 1 100%" : "1 1 50%",
          maxWidth: child.tag === "nested" ? "100%" : "50%",
          minWidth: 0,
        }}
      >
        {AttributeDataTag.matchWithArg({
          root: (v) => (
            <RootComponent
              name={v.name}
              children={v.children}
              expanded={isExpanded(v.id)}
              onChange={handleAccordionChange(v.id)}
              responsibilityZone={responsibilityZone}
              rootId={v.id}
              isSwitchActive={isSwitchActive(
                v as AttributeDataDto,
                isDirectChildOfRootId0,
              )}
            />
          ),
          simple: (v) => (
            <Box className={styles.simpleComponent}>
              <SimpleComponent name={v.name} value={v.value} level={level} />
            </Box>
          ),
          nested: (v) => (
            <NestedComponent
              name={v.name}
              children={v.children}
              level={level}
              responsibilityZone={zone}
              isSwitchActive={isSwitchActive(
                v as AttributeDataDto,
                isDirectChildOfRootId0,
              )}
            />
          ),
        })(child)}
      </Box>
    ));
  };

  const RootComponent = ({
    name,
    children,
    expanded,
    onChange,
    responsibilityZone,
    rootId,
    isSwitchActive,
  }: {
    name: string;
    children: AttributeDataDto[];
    expanded: boolean;
    onChange: (event: React.SyntheticEvent, isExpanded: boolean) => void;
    responsibilityZone: number | null;
    rootId: number;
    isSwitchActive: boolean;
  }) => {
    if (children.length === 0 && isSwitchActive) return;
    return (
      <Accordion elevation={0} expanded={expanded} onChange={onChange}>
        <AccordionSummary
          expandIcon={<ExpandMore />}
          className={styles.titleBlock}
        >
          <Typography className={styles.title}>{name}</Typography>
        </AccordionSummary>
        <AccordionDetails>
          {isSwitchActive ? (
            <Box
              sx={{
                display: "flex",
                flexWrap: "wrap",
                m: "0 16px",
              }}
            >
              {renderChildren(children, 1, responsibilityZone, rootId === 0)}
            </Box>
          ) : (
            <Box
              sx={{
                m: "0 16px",
              }}
            >
              <Typography>Нет</Typography>
            </Box>
          )}
        </AccordionDetails>
      </Accordion>
    );
  };

  const NestedComponent = ({
    name,
    children,
    level,
    responsibilityZone,
    isSwitchActive,
  }: {
    name: string;
    children: AttributeDataDto[];
    level: number;
    responsibilityZone: number | null;
    isSwitchActive: boolean;
  }) => {
    if (children.length === 0 && isSwitchActive) return;
    return (
      <Accordion className={styles.toggleSection}>
        <AccordionSummary
          expandIcon={<ExpandMore />}
          className={clsx({ [styles.toggleSectionBackground]: level > 1 })}
        >
          <Typography className={styles.toggleSectionTitle}>{name}</Typography>
        </AccordionSummary>
        <AccordionDetails className={styles.toggleSectionContent}>
          {isSwitchActive ? (
            renderChildren(children, level + 1, responsibilityZone)
          ) : (
            <Box>
              <Typography>Нет</Typography>
            </Box>
          )}
        </AccordionDetails>
      </Accordion>
    );
  };

  return (
    <Box
      className={clsx(styles.productDetails, {
        [styles.isLoading]: isLoading,
      })}
    >
      {isLoading ? (
        <CircularProgress size={100} />
      ) : (
        <>
          <CommonDetails
            id={id}
            responsibilityZone={responsibilityZone}
            setResponsibilityZone={setResponsibilityZone}
            title={title}
          />

          {sortedData?.map((item) => (
            <div
              key={item.id}
              ref={(el: HTMLDivElement | null) => {
                sectionRefs.current[item.id] = el;
              }}
            >
              {AttributeDataTag.matchWithArg({
                root: (v) => (
                  <RootComponent
                    name={v.name}
                    children={v.children}
                    expanded={isExpanded(v.id)}
                    onChange={handleAccordionChange(v.id)}
                    responsibilityZone={responsibilityZone}
                    rootId={v.id}
                    isSwitchActive={isSwitchActive(v as AttributeDataDto)}
                  />
                ),
                simple: (v) => (
                  <SimpleComponent name={v.name} value={v.value} level={1} />
                ),
                nested: (v) => (
                  <NestedComponent
                    name={v.name}
                    children={v.children}
                    level={1}
                    responsibilityZone={responsibilityZone}
                    isSwitchActive={isSwitchActive(v as AttributeDataDto)}
                  />
                ),
              })(item)}
            </div>
          ))}
        </>
      )}
    </Box>
  );
};

----AttributeDataDto.ts
import * as gnr from "@gnr/api";
import { AttributeDataTag } from "@lib/tags/AttributeDataTag.ts";
import { SimpleValue } from "@lib/models/SimpleValue.ts";
import { log } from "@logger";
import { getErrorMessage } from "@shared/utils/ErrorUtils.ts";
import { normalizeUseFlagValue } from "@shared/utils/attributeUseValue";

type AttributeDataBase<T extends AttributeDataTag> = {
  readonly tag: T;
  readonly id: number;
  readonly displayOrder: number;
  readonly name: string;
  readonly showInTable: boolean;
  readonly useForComparison: boolean;
  readonly responsibilityZone: number | null;
  readonly targetCategoryId: number | null;
};

export type RootAttributeData = AttributeDataBase<"root"> &
  gnr.AttributeDataDtoOneOf & {
    readonly children: AttributeDataDto[];
  };

export type SimpleAttributeData = AttributeDataBase<"simple"> &
  Omit<gnr.AttributeDataDtoOneOfTwo, "tag"> & {
    readonly value: SimpleValue | null;
  };

export type NestedAttributeData = AttributeDataBase<"nested"> &
  gnr.AttributeDataDtoOneOfThree & {
    readonly children: AttributeDataDto[];
  };

type AttributeData =
  | RootAttributeData
  | SimpleAttributeData
  | NestedAttributeData;

type AttributeDataDto = gnr.AttributeDataDto & AttributeData;

// eslint-disable-next-line no-redeclare
namespace AttributeDataDto {
  export function parse(value: unknown): AttributeDataDto[] {
    const dto: gnr.AttributeDataDto[] =
      gnr.objectGetDetailsResponse.parse(value);
    try {
      return dto.map(parseItem);
    } catch (e) {
      const errorMessage = `En: 'w6qgTBPf', value: ${value}, error: ${getErrorMessage(e)}`;
      const error = new Error(errorMessage);

      log("ERROR", error);

      throw error;
    }
  }

  const parseItem = (dto: gnr.AttributeDataDto): AttributeDataDto => {
    return AttributeDataTag.match({
      root: () => {
        return makeRoot(dto as gnr.AttributeDataDtoOneOf) as AttributeDataDto;
      },
      simple: () => {
        return makeSimple(dto as gnr.AttributeDataDtoOneOfTwo);
      },
      nested: () => {
        return makeNested(dto as gnr.AttributeDataDtoOneOfThree);
      },
    })(dto.tag);
  };

  function makeBase<A extends AttributeDataTag>(
    tag: A,
    dto: gnr.AttributeDataDto,
  ): AttributeDataBase<A> {
    return {
      ...dto,
      tag,
      id: dto.id,
      displayOrder: dto.displayOrder,
      name: dto.name,
      showInTable: dto.showInTable,
      useForComparison: dto.useForComparison,
      responsibilityZone: dto.responsibilityZone,
      targetCategoryId: dto.targetCategoryId,
    };
  }

  // eslint-disable-next-line no-unused-vars
  type MakeRoot = (dto: gnr.AttributeDataDtoOneOf) => RootAttributeData;

  const makeRoot: MakeRoot = (dto) => ({
    ...makeBase("root", dto),
    children: dto.children.map(parseItem),
    value: {
      tag: "boolean",
      value: normalizeUseFlagValue(dto.value),
    },
    required: dto.required,
  });

  // eslint-disable-next-line no-unused-vars
  type MakeSimple = (dto: gnr.AttributeDataDtoOneOfTwo) => SimpleAttributeData;

  const makeSimple: MakeSimple = (dto) => ({
    ...makeBase("simple", dto),
    value: SimpleValue.parse(dto.value),
    required: dto.required,
  });

  // eslint-disable-next-line no-unused-vars
  type MakeNested = (
    dto: gnr.AttributeDataDtoOneOfThree,
  ) => NestedAttributeData;

  const makeNested: MakeNested = (dto) => ({
    ...makeBase("nested", dto),
    children: dto.children.map(parseItem),
    value: {
      tag: "boolean",
      value: normalizeUseFlagValue(dto.value),
    },
    required: dto.required,
  });
}

export { AttributeDataDto };

----attributeUseValue.ts
import type {
  NestedAttributeData,
  RootAttributeData,
} from "@lib/dtos/AttributeDataDto.ts";
import { AttributeDataDto } from "@gnr/api";
import { AttributeDataTag } from "@lib/tags/AttributeDataTag";

export type UsedRoot = {
  attributeId: number;
  value: { tag: "boolean"; value: string };
};

type AttributeWithUseValue = RootAttributeData | NestedAttributeData;

const parseBooleanUseValue = (raw: unknown): boolean | null => {
  if (raw == null) return null;
  if (typeof raw === "boolean") return raw;
  if (typeof raw === "string") {
    const normalized = raw.toLowerCase();
    if (normalized === "true") return true;
    if (normalized === "false") return false;
    return null;
  }
  if (typeof raw === "object") {
    const obj = raw as Record<string, unknown>;
    if (obj.tag === "boolean") {
      return parseBooleanUseValue(obj.value);
    }
    if (obj.tag === "nullableBool" && typeof obj.nullableValue === "boolean") {
      return obj.nullableValue;
    }
    if ("value" in obj) return parseBooleanUseValue(obj.value);
    if (typeof obj.nullableValue === "boolean") return obj.nullableValue;
  }
  return null;
};

/** Нормализует флаг «Использовать» из API в "True" | "False" | null */
export const normalizeUseFlagValue = (
  dtoValue: unknown,
): "True" | "False" | null => {
  const parsed = parseBooleanUseValue(dtoValue);
  if (parsed === null) return null;
  return parsed ? "True" : "False";
};

export const getAttributeUseValue = (
  type: "filter" | "update" | "create",
  objectId: number,
  attribute: AttributeWithUseValue,
  isDirectChildOfRootId0 = false,
): "True" | "False" => {
  if (type === "create" && !objectId) return "True";

  const flag = normalizeUseFlagValue(attribute.value);
  if (flag === "True" || flag === "False") return flag;

  if (isDirectChildOfRootId0) return "True";

  return "False";
};

export const isAttributeUsed = (
  attribute: { value?: unknown },
  isDirectChildOfRootId0 = false,
): boolean => {
  const flag = normalizeUseFlagValue(attribute.value);
  if (flag === "True") return true;
  if (flag === "False") return false;
  return isDirectChildOfRootId0;
};

export const toggleUsedRoot = (
  prev: UsedRoot[],
  attributeId: number,
  checked: boolean,
): UsedRoot[] => {
  const value = checked ? "True" : "False";
  const entry: UsedRoot = {
    attributeId,
    value: { tag: "boolean", value },
  };
  const exists = prev.some((item) => item.attributeId === attributeId);
  return exists
    ? prev.map((item) => (item.attributeId === attributeId ? entry : item))
    : [...prev, entry];
};

export const isUsedRootChecked = (
  usedRoots: UsedRoot[] | undefined,
  attributeId: number,
): boolean =>
  usedRoots?.find((item) => item.attributeId === attributeId)?.value.value ===
  "True";

export const collectUsedRoots = (
  groups: readonly AttributeDataDto[],
  type: "update" | "create" | "filter",
  objectId: number,
): UsedRoot[] => {
  const result: UsedRoot[] = [];

  const traverse = (
    items: readonly AttributeDataDto[],
    isDirectChildOfRootId0 = false,
  ) => {
    items.forEach((group) => {
      AttributeDataTag.match({
        root: () => {
          const root = group as RootAttributeData;
          if (root.id !== 0) {
            result.push({
              attributeId: root.id,
              value: {
                tag: "boolean",
                value: getAttributeUseValue(
                  type,
                  objectId,
                  root,
                  isDirectChildOfRootId0,
                ),
              },
            });
          }
          traverse(root.children ?? [], root.id === 0);
        },
        nested: () => {
          const nested = group as NestedAttributeData;
          if (nested.id !== 0) {
            result.push({
              attributeId: nested.id,
              value: {
                tag: "boolean",
                value: getAttributeUseValue(
                  type,
                  objectId,
                  nested,
                  isDirectChildOfRootId0,
                ),
              },
            });
          }
          traverse(nested.children ?? [], false);
        },
        simple: () => {},
      })(group.tag);
    });
  };

  traverse(groups);
  return result;
};