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


import { OTHER_VALUES } from '@/domains/controller/components/CreateController/model/formNames';
import { useControllerRepository } from '@/domains/equipment/repositories/controller.repository';
import { useNetInterfaceRepository } from '@/domains/equipment/repositories/netInterface.repository';
import { ConfirmIpChangeModal } from '@/entities/Modals';
import { useGetEquipmentWindowQuery } from '@/shared/api/auth/equipment/client';
import { Helpers } from '@/shared/lib';
import { yupResolver } from '@hookform/resolvers/yup';
import { QueryStatus, skipToken } from '@reduxjs/toolkit/query';
import {
  DeviceModel,
  useAddControllerModelMutation,
  useGetApiV1SecondaryInterfaceValueOwnerByOwnerIdQuery,
  useGetControllerByIdQuery,
  useGetControllerModelByIdQuery,
} from '@shared/api/controller/client';
import {
  Button,
  Drawer,
  DrawerActions,
  DrawerProps,
  Loader,
} from '@shared/components';
import { useCloseDrawer } from '@shared/hooks/drawer/useCloseDrawer';
import { useSelectDrawerType } from '@shared/hooks/drawer/useSelectDrawerType';
import {
  EventActionTypes,
  useSendClickStreamEvent,
} from '@shared/hooks/useSendClickStreamEvent';
import { DrawerTypes } from '@shared/model/drawer/drawer.types';
import { useControllerActions } from '@shared/store/сontrollers/controllers.actions';
import {
  useSelectActiveControllerId,
  useSelectControllerLocationId,
} from '@shared/store/сontrollers/сontrollers.selectors';
import { useSyncControlPage } from '@widgets/Monitoring/hooks';
import { memo, useEffect, useState } from 'react';
import {
  FormProvider,
  Resolver,
  SubmitHandler,
  useForm,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { UpdateControllerForm } from './UpdateControllerForm';
import { OTHER_NAMES } from './model/formNames';
import { UpdateControllerFormData } from './model/types';
import { updateControllerFormSchema } from './validation/validator';

const FIVE_SECONDS = 5000;
const EXTRON_MANUFACTURER = 'EXTRON';

type ConfirmChangeReason = 'ip' | 'manufacturer' | 'ipAndManufacturer';

const normalizeManufacturerName = (value?: string | null) => {
  return value?.trim().toUpperCase() ?? '';
};

const isExtronManufacturer = (value?: string | null) => {
  return normalizeManufacturerName(value) === EXTRON_MANUFACTURER;
};

const getConfirmChangeReason = (
  isIpChanged: boolean,
  isManufacturerChangedFromExtron: boolean,
): ConfirmChangeReason => {
  if (isIpChanged && isManufacturerChangedFromExtron) {
    return 'ipAndManufacturer';
  }

  if (isManufacturerChangedFromExtron) {
    return 'manufacturer';
  }

  return 'ip';
};

export const UpdateControllerDrawer = memo(() => {
  const { t } = useTranslation();

  const { sendEvent } = useSendClickStreamEvent(
    EventActionTypes.createController,
  );

  const { cleanControllerLocationId } = useControllerActions();

  const closeDrawer = useCloseDrawer();
  const drawerType = useSelectDrawerType();
  const open = drawerType[DrawerTypes.updateController];

  const controllerId = useSelectActiveControllerId();
  const locationId = useSelectControllerLocationId();

  const {
    updateController,
    isUpdateControllerLoading,
    controllerUpdateStatus,
    resetUpdateController,
  } = useControllerRepository();

  const { data: controller } = useGetControllerByIdQuery(
    { controllerId: controllerId ?? '' },
    {
      skip: !controllerId,
      refetchOnMountOrArgChange: true,
    },
  );

  const id = controller?.id;
  const currentIp = controller?.ip?.trim() ?? '';

  const { data: equipmentWindowData } = useGetEquipmentWindowQuery(
    { ip: currentIp },
    {
      skip: !currentIp || !open,
      pollingInterval: FIVE_SECONDS,
    },
  );

  const hasActiveWindows =
    !!equipmentWindowData?.status && equipmentWindowData.status !== 'absent';

  const {
    data: secondaryInterfaceData,
    isSuccess: isSecondaryInterfaceSuccess,
  } = useGetApiV1SecondaryInterfaceValueOwnerByOwnerIdQuery(
    { ownerId: id! },
    { skip: !id },
  );

  const [model, setModel] = useState<DeviceModel | null>(null);
  const [pendingValues, setPendingValues] =
    useState<UpdateControllerFormData | null>(null);
  const [confirmChangeReason, setConfirmChangeReason] =
    useState<ConfirmChangeReason | null>(null);

  const [createdModelId, setCreatedModelId] = useState<string | null>(null);

  const [, createDeviceModelResult] = useAddControllerModelMutation({
    fixedCacheKey: 'create-controller-model',
  });

  useEffect(() => {
    const id = createDeviceModelResult.data?.id;

    if (id) {
      setCreatedModelId(id);
    }
  }, [createDeviceModelResult.data?.id]);

  const { data: createdModel } = useGetControllerModelByIdQuery(
    createdModelId ? { modelId: createdModelId } : skipToken,
    {
      refetchOnMountOrArgChange: true,
    },
  );

  useEffect(() => {
    if (createdModel) {
      setModel(createdModel);
    }
  }, [createdModel]);

  const defaultValues: UpdateControllerFormData = {
    name: controller?.name,
    description: controller?.description,
    locationId: controller?.locationId,
    firmware: controller?.firmware,
    controlPage: controller?.controlPage,
    subnetMask: controller?.subnetMask,
    gateway: controller?.gateway,
    telnetPort: controller?.telnetPort,
    port: controller?.interface?.port,
    timeZone: controller?.timeZone,
    sendNotifications: controller?.sendNotifications,
    ip: controller?.ip,
    mac: controller?.mac,
    typeName: controller?.model?.type?.name,
    manufacturerName: controller?.model?.manufacturer?.name,
    modelId: controller?.model?.id,
    segment: controller?.segment,
    serialNumber: controller?.serialNumber,
    inventoryNumber: controller?.inventoryNumber,
    protocolIp: controller?.interface?.protocolIp,
    localAddress: controller?.interface?.localAddress,
    ieee8021x: controller?.interface?.ieee8021x,
    isMain: controller?.isMain,
    netInterface: !!secondaryInterfaceData?.length,
    netInterfaceInfo: secondaryInterfaceData?.map((secondaryInterface) => {
      return {
        gateway: secondaryInterface.gateway,
        mac: secondaryInterface.mac,
        ip: secondaryInterface.ip,
        subnetMask: secondaryInterface.subnetMask,
        nameId: secondaryInterface.name?.id,
        id: secondaryInterface.id,
      };
    }),
    disableMonitoring: controller?.disableMonitoring ?? false,
    qrCodeType: controller?.qrCodeType,
  };

  const methods = useForm<UpdateControllerFormData>({
    mode: 'onBlur',
    reValidateMode: 'onChange',
    defaultValues,
    resolver: yupResolver(
      updateControllerFormSchema,
    ) as Resolver<UpdateControllerFormData>,
  });

  const handleClearModel = () => {
    methods.setValue('modelId', '');
    methods.setValue('manufacturerName', '');
    methods.setValue('typeName', '');
  };

  useEffect(() => {
    if (controller?.model) {
      setModel(controller.model);
    }
  }, [controller]);

  useEffect(() => {
    if (model) {
      const isModelChanged = model.id !== controller?.model?.id;

      methods.setValue('modelId', model?.id || '', {
        shouldDirty: isModelChanged,
        shouldValidate: true,
      });
      methods.setValue('manufacturerName', model?.manufacturer?.name);
      methods.setValue('typeName', model?.type?.name);
    }
  }, [model]);

  useEffect(() => {
    if (isSecondaryInterfaceSuccess && secondaryInterfaceData) {
      methods.setValue(
        OTHER_VALUES.netInterface,
        !!secondaryInterfaceData?.length,
      );

      methods.setValue(
        OTHER_VALUES.netInterfaceInfo,
        secondaryInterfaceData?.map((secondaryInterface) => {
          return {
            gateway: secondaryInterface.gateway,
            mac: secondaryInterface.mac,
            ip: secondaryInterface.ip,
            subnetMask: secondaryInterface.subnetMask,
            nameId: secondaryInterface.name?.id,
            id: secondaryInterface.id,
          };
        }),
      );
    }
  }, [isSecondaryInterfaceSuccess, secondaryInterfaceData]);

  const netInterfaceInfo = methods.watch(OTHER_NAMES.netInterfaceInfo);
  const netInterface = methods.watch(OTHER_NAMES.netInterface);

  const { deleteNetInterfaces } = useNetInterfaceRepository();

  useEffect(() => {
    (async () => {
      if (
        !netInterface &&
        isSecondaryInterfaceSuccess &&
        netInterfaceInfo &&
        !!netInterfaceInfo.length
      ) {
        await deleteNetInterfaces(
          netInterfaceInfo?.map((netInterface) => netInterface.id),
        );

        methods.setValue(OTHER_VALUES.netInterfaceInfo, []);
      }
    })();
  }, [netInterface, netInterfaceInfo, isSecondaryInterfaceSuccess]);

  useSyncControlPage(controller?.ip, methods);

  useEffect(() => {
    if (!Helpers.isEmpty(locationId)) {
      methods.setValue(OTHER_NAMES.locationId, locationId);
      methods.reset(methods.getValues());
    }
  }, [locationId]);

  useEffect(() => {
    if (controllerUpdateStatus === QueryStatus.fulfilled && controller?.id) {
      closeDrawer(DrawerTypes.updateController);
      resetUpdateController();
      sendEvent();
    }
  }, [controller?.id, controllerUpdateStatus]);

  const updateNetworkDevice = async (values: UpdateControllerFormData) => {
    if (!id) {
      return;
    }

    await updateController(values, id);
    cleanControllerLocationId();
  };

  const submitHandler: SubmitHandler<UpdateControllerFormData> = async (
    values,
  ) => {
    const oldIp = controller?.ip?.trim() ?? '';
    const newIp = values.ip?.trim() ?? '';

    const isIpChanged = oldIp !== newIp;

    const isManufacturerChangedFromExtron =
      isExtronManufacturer(controller?.model?.manufacturer?.name) &&
      !isExtronManufacturer(values.manufacturerName);

    if (hasActiveWindows && (isIpChanged || isManufacturerChangedFromExtron)) {
      setPendingValues(values);
      setConfirmChangeReason(
        getConfirmChangeReason(isIpChanged, isManufacturerChangedFromExtron),
      );

      return;
    }

    await updateNetworkDevice(values);
  };

  const handleConfirmIpChange = async () => {
    if (!pendingValues) {
      return;
    }

    const values = pendingValues;

    setPendingValues(null);
    setConfirmChangeReason(null);

    await updateNetworkDevice(values);
  };

  const handleCancelIpChange = () => {
    setPendingValues(null);
    setConfirmChangeReason(null);
  };

  useEffect(() => {
    methods.reset(defaultValues);
  }, [controller, open]);

  const { isValid, dirtyFields } = methods.formState;

  const isDirtyLocationId =
    methods.getValues().locationId !== defaultValues.locationId;

  const isDirtyFields = !Helpers.isEmpty(dirtyFields) || isDirtyLocationId;
  const isSaveButtonDisabled = !isDirtyFields || !isValid;

  const actions: DrawerActions = {
    actionPrimary: (
      <Button
        onClick={methods.handleSubmit(submitHandler)}
        disabled={isSaveButtonDisabled}
        loading={isUpdateControllerLoading}
      >
        {controllerUpdateStatus === QueryStatus.pending ? (
          <Loader />
        ) : (
          t('common:buttons.save')
        )}
      </Button>
    ),
  };

  const handleClose: DrawerProps['onClose'] = () => {
    cleanControllerLocationId();
    methods.reset(defaultValues);
    setModel(null);
    setPendingValues(null);
    setConfirmChangeReason(null);
  };

  if (!controller) {
    return null;
  }

  const initialModel = model ? model : controller.model;

  return (
    <>
      <FormProvider {...methods}>
        <Drawer
          type={DrawerTypes.updateController}
          actions={actions}
          title={t('common:drawerTitle.updateController')}
          onClose={handleClose}
        >
          <UpdateControllerForm
            initialModel={initialModel}
            setModel={setModel}
            handleClearModel={handleClearModel}
          />
        </Drawer>
      </FormProvider>

      <ConfirmIpChangeModal
        open={!!pendingValues}
        reason={confirmChangeReason}
        oldIp={controller?.ip ?? ''}
        newIp={pendingValues?.ip ?? ''}
        onConfirm={handleConfirmIpChange}
        onClose={handleCancelIpChange}
      />
    </>
  );
});


import { Box, Button, Modal, Typography } from '@/shared/components';
import { useTranslation } from 'react-i18next';

type ConfirmChangeReason = 'ip' | 'manufacturer' | 'ipAndManufacturer';

interface ConfirmIpChangeModalProps {
  open: boolean;
  reason: ConfirmChangeReason | null;
  oldIp: string;
  newIp: string;
  onConfirm: () => void | Promise<void>;
  onClose: () => void;
}

const getMessageKey = (reason: ConfirmChangeReason | null) => {
  if (reason === 'manufacturer') {
    return 'messages.confirmManufacturerChangeFromExtron';
  }

  if (reason === 'ipAndManufacturer') {
    return 'messages.confirmIpAndManufacturerChangeFromExtron';
  }

  return 'messages.confirmIpChange';
};

const getTitleKey = (reason: ConfirmChangeReason | null) => {
  if (reason === 'ip') {
    return 'modalTitle.confirmIpChange';
  }

  return 'modalTitle.confirmAuthWindowsReset';
};

export const ConfirmIpChangeModal = ({
  open,
  reason,
  oldIp,
  newIp,
  onConfirm,
  onClose,
}: ConfirmIpChangeModalProps) => {
  const { t } = useTranslation('common');

  return (
    <Modal
      open={open}
      title={t(getTitleKey(reason))}
      onClose={onClose}
      actionPrimary={<Button onClick={onConfirm}>{t('yes')}</Button>}
      actionSecondary={<Button onClick={onClose}>{t('no')}</Button>}
    >
      <Box p='0px 16px' textAlign='center'>
        <Typography variant='caption2'>
          {t(getMessageKey(reason), {
            oldIp,
            newIp,
          })}
        </Typography>
      </Box>
    </Modal>
  );
};