feat(TEAMMSBMOB-22080): блокирующие уведомления и их подписание
This commit is contained in:
@@ -238,7 +238,7 @@
|
||||
},
|
||||
"txt": {
|
||||
"dstList": {
|
||||
"certExpireWarning_0":"Истекает через {{count}} день",
|
||||
"certExpireWarning_0": "Истекает через {{count}} день",
|
||||
"certExpireWarning_1": "Истекает через {{count}} дня",
|
||||
"certExpireWarning": "Истекает через {{count}} дней"
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ enum FEATURE_TOGGLE_NAMES {
|
||||
DEPOSIT_FORM = 'depositFormIBMSB',
|
||||
DEPOSIT_ADVANCED_FILTER = 'depositFilterIBMSB',
|
||||
VED_CALLBACK_IMSB = 'vedCallbackIBMSB',
|
||||
BLOCKING_NOTIFICATIONS = 'blockingNotificationsIBMSB',
|
||||
}
|
||||
|
||||
export { FEATURE_TOGGLE_NAMES, type FeatureToggleData, type FeatureToggleItem, type FeatureToggleResponse };
|
||||
|
||||
@@ -8,6 +8,16 @@ enum NOTIFICATION_TYPE {
|
||||
ALERT = 'ALERT',
|
||||
}
|
||||
|
||||
/** Статусы прочтения. */
|
||||
enum READ_STATUS {
|
||||
/** Новое. */
|
||||
NEW = 'NEW',
|
||||
/** Прочтено. */
|
||||
READ = 'READ',
|
||||
/** В архиве. */
|
||||
ARCHIVE = 'ARCHIVE',
|
||||
}
|
||||
|
||||
/** Статусы выполнения. */
|
||||
enum EXECUTE_STATUS {
|
||||
/** Не выполнено. */
|
||||
@@ -34,4 +44,4 @@ enum IMPORTANCE_VALUES {
|
||||
LOW = 'LOW',
|
||||
}
|
||||
|
||||
export { NOTIFICATION_TYPE, EXECUTE_STATUS, IMPORTANCE_VALUES };
|
||||
export { NOTIFICATION_TYPE, EXECUTE_STATUS, IMPORTANCE_VALUES, READ_STATUS };
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
const NOTIFICATION_URL_V2 = 'notification/v2';
|
||||
const USER_NOTIFICATIONS_CATEGORY_COUNT = '/notification/user-notification/category-count' as const;
|
||||
const USER_NOTIFICATIONS_GET_LIST = 'notification/v2/user-notification/get-page' as const;
|
||||
const USER_NOTIFICATION_BY_ID = 'notification/v2/user-notification/:id' as const;
|
||||
const MARK_NOTIFICATION_AS_READ = 'notification/v2/user-notification/mark-as-read' as const;
|
||||
const USER_NOTIFICATONS_CREATE_ZIP = 'notification/v2/user-notification/create-zip';
|
||||
const USER_NOTIFICATONS_EXECUTE = 'notification/v2/user-notification/execute';
|
||||
const USER_NOTIFICATIONS_GET_LIST = `${NOTIFICATION_URL_V2}/user-notification/get-page` as const;
|
||||
const USER_NOTIFICATION_BY_ID = `${NOTIFICATION_URL_V2}/user-notification/:id` as const;
|
||||
const MARK_NOTIFICATION_AS_READ = `${NOTIFICATION_URL_V2}/user-notification/mark-as-read` as const;
|
||||
const USER_NOTIFICATONS_CREATE_ZIP = `${NOTIFICATION_URL_V2}/user-notification/create-zip` as const;
|
||||
const USER_NOTIFICATONS_EXECUTE = `${NOTIFICATION_URL_V2}/user-notification/execute` as const;
|
||||
const USER_NOTIFICATONS_SIGN = `${NOTIFICATION_URL_V2}/user-notification/sign` as const;
|
||||
const USER_NOTIFICATONS_GENERATE_SIGN_DATA = `${NOTIFICATION_URL_V2}/user-notification/generate-sign-data` as const;
|
||||
const USER_NOTIFICATONS_GENERATE_SIGN_DATA_V2 = `${NOTIFICATION_URL_V2}/user-notification/v2.1/generate-sign-data` as const;
|
||||
const USER_NOTIFICATONS_REQUIRED_READ = `${NOTIFICATION_URL_V2}/user-notification/required-read` as const;
|
||||
|
||||
export {
|
||||
USER_NOTIFICATIONS_CATEGORY_COUNT,
|
||||
@@ -12,4 +17,8 @@ export {
|
||||
MARK_NOTIFICATION_AS_READ,
|
||||
USER_NOTIFICATONS_CREATE_ZIP,
|
||||
USER_NOTIFICATONS_EXECUTE,
|
||||
USER_NOTIFICATONS_SIGN,
|
||||
USER_NOTIFICATONS_GENERATE_SIGN_DATA,
|
||||
USER_NOTIFICATONS_GENERATE_SIGN_DATA_V2,
|
||||
USER_NOTIFICATONS_REQUIRED_READ,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const IS_WELCOME_MESSAGE_READ_KEY = 'isWelcomeMessageRead';
|
||||
const BLOCKING_NOTIFICATION_MODAL_KEY = 'blocking_notification_modal_is_open';
|
||||
|
||||
export { IS_WELCOME_MESSAGE_READ_KEY };
|
||||
export { IS_WELCOME_MESSAGE_READ_KEY, BLOCKING_NOTIFICATION_MODAL_KEY };
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { AuthoritiesResponseDto, OrganizationDto, UserProfileDto } from '@msb/http';
|
||||
import type { AuthoritiesResponseDto, NotificationDto, OrganizationDto, UserProfileDto } from '@msb/http';
|
||||
|
||||
interface AppContextValue {
|
||||
userProfile: UserProfileDto | undefined;
|
||||
isUserProfileError: boolean;
|
||||
refetchUserProfile(): void;
|
||||
|
||||
userAuthorities: AuthoritiesResponseDto | undefined;
|
||||
userAuthoritiesError: Error | null | undefined;
|
||||
refetchUserAuthorities(): void;
|
||||
|
||||
organizations: OrganizationDto[];
|
||||
isOrganizationsLoading: boolean;
|
||||
organizationsError: Error | null | undefined;
|
||||
refetchOrganizations(): void;
|
||||
selectedOrganization: OrganizationDto | null;
|
||||
changeSelectedOrganization(organization: OrganizationDto | null): void;
|
||||
requiredNotifications: NotificationDto[];
|
||||
isRequiredNotificationsLoaded: boolean;
|
||||
isRequiredNotificationsFailed: boolean;
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextValue | null>(null);
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
{
|
||||
"featureCode": "metricaSetIdIBMSB",
|
||||
"isEnabled": true
|
||||
},
|
||||
{
|
||||
"featureCode": "blockingNotificationsIBMSB",
|
||||
"isEnabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OrganizationDto } from '@msb/http';
|
||||
import type { AppContextValue } from '@msb/shared';
|
||||
import { AppContext, useScrollLock } from '@msb/shared';
|
||||
import { useOrganizations, useUserAuthorities, useUserProfile } from '../model';
|
||||
import { useUserNotificationRequiredRead } from '@/entities/UserNotification';
|
||||
import { SELECTED_ORGANIZATION_KEY } from '@/shared/constants';
|
||||
import { LayoutSkeletons } from '@/shared/ui/components';
|
||||
|
||||
@@ -16,6 +17,12 @@ const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||
isLoading: isOrganizationsLoading,
|
||||
} = useOrganizations(Boolean(userAuthoritiesData));
|
||||
|
||||
const {
|
||||
notifications,
|
||||
isSuccess: isRequiredNotificationsLoaded,
|
||||
error: requiredNotificationsError,
|
||||
} = useUserNotificationRequiredRead(Boolean(userAuthoritiesData));
|
||||
|
||||
const [selectedOrganization, setSelectedOrganization] = useState<OrganizationDto | null>(
|
||||
sessionStorage.getItem(SELECTED_ORGANIZATION_KEY) ? JSON.parse(sessionStorage.getItem(SELECTED_ORGANIZATION_KEY)!) : null
|
||||
);
|
||||
@@ -46,6 +53,9 @@ const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||
refetchOrganizations,
|
||||
changeSelectedOrganization,
|
||||
selectedOrganization,
|
||||
requiredNotifications: notifications,
|
||||
isRequiredNotificationsLoaded,
|
||||
isRequiredNotificationsFailed: !!requiredNotificationsError,
|
||||
}),
|
||||
[
|
||||
userProfileData,
|
||||
@@ -58,7 +68,10 @@ const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||
organizations,
|
||||
isOrganizationsLoading,
|
||||
refetchOrganizations,
|
||||
notifications,
|
||||
selectedOrganization,
|
||||
requiredNotificationsError,
|
||||
isRequiredNotificationsLoaded,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"noCloudCertView": {
|
||||
"txt": {
|
||||
"noCloudCertTitle": "Подпишите документ на персональном компьютере",
|
||||
"noCloudCert": "Вы можете подписать документ только на компьютере при помощи локальной подписи.\n\nДля подписания документа на мобильном устройстве необходима облачная электронная подпись. Выпустить её можно также на компьютере.\n\nДокумент сохранён."
|
||||
"noCloudCert": "Вы можете подписать документ только на компьютере при помощи локальной подписи.\n\nДля подписания документа на мобильном устройстве необходима облачная электронная подпись. Выпустить её можно также на компьютере."
|
||||
}
|
||||
},
|
||||
"noSignDataView": {
|
||||
@@ -89,8 +89,8 @@
|
||||
"noCertificatesView": {
|
||||
"txt": {
|
||||
"noCertTitle": "Не найдена подходящая электронная подпись",
|
||||
"mobileNoCertContent": "Для подписания документа на мобильном устройстве выпустите <0>облачную электронную подпись</0>.\n\nДокумент сохранён.",
|
||||
"desktopNoCertContent": "Для подписания документа выпустите или зарегистрируйте <0>электронную подпись</0>.\n\nДокумент сохранён."
|
||||
"mobileNoCertContent": "Для подписания документа на мобильном устройстве выпустите <0>облачную электронную подпись</0>.",
|
||||
"desktopNoCertContent": "Для подписания документа выпустите или зарегистрируйте <0>электронную подпись</0>."
|
||||
},
|
||||
"btn": {
|
||||
"toProfile": "В раздел Электронные подписи"
|
||||
@@ -241,7 +241,7 @@
|
||||
}
|
||||
},
|
||||
"btn": {
|
||||
"toList": "К списку заявок"
|
||||
"toList": "Назад"
|
||||
},
|
||||
"lbl": {
|
||||
"signToolAbb": "СЭП",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export const QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT = 'user-notification-category-count';
|
||||
export const QUERY_KEY_USER_NOTIFICATION_LIST = 'user-notification-list';
|
||||
export const QUERY_KEY_USER_NOTIFICATION_BY_ID = 'user-notification-by-id';
|
||||
export const QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION = 'required-read-user-notifications';
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
USER_NOTIFICATION_BY_ID,
|
||||
USER_NOTIFICATONS_CREATE_ZIP,
|
||||
USER_NOTIFICATONS_EXECUTE,
|
||||
USER_NOTIFICATONS_REQUIRED_READ,
|
||||
} from '@msb/http';
|
||||
import type { ServerResponsePaginationData } from '@msb/http/model';
|
||||
|
||||
@@ -17,6 +18,14 @@ const fetchUserNotificationsCategoryCount = async (): Promise<CategoryCountDto>
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const fetchRequiredReadUserNotifications = async (): Promise<ServerResponsePaginationData<NotificationDto>> => {
|
||||
const response = await network.client.post<ServerResponsePaginationData<NotificationDto>>(USER_NOTIFICATONS_REQUIRED_READ, {
|
||||
params: { paging: { offset: 0, limit: 100 } },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createZip = async (attachmentIds: string[], fileName: string) => {
|
||||
const response = await network.client.post(
|
||||
USER_NOTIFICATONS_CREATE_ZIP,
|
||||
@@ -63,4 +72,11 @@ const fetchUserNotifications = async ({
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export { fetchUserNotificationsCategoryCount, fetchUserNotifications, fetchUserNotificationById, createZip, executeNotification };
|
||||
export {
|
||||
fetchUserNotificationsCategoryCount,
|
||||
fetchUserNotifications,
|
||||
fetchUserNotificationById,
|
||||
createZip,
|
||||
executeNotification,
|
||||
fetchRequiredReadUserNotifications,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ISignRequestData } from '@msb/crypto';
|
||||
import { network, USER_NOTIFICATONS_SIGN, USER_NOTIFICATONS_GENERATE_SIGN_DATA, USER_NOTIFICATONS_GENERATE_SIGN_DATA_V2 } from '@msb/http';
|
||||
|
||||
const signService = {
|
||||
sign: async (data: ISignRequestData) => {
|
||||
const response = await network.client.post(USER_NOTIFICATONS_SIGN, data);
|
||||
|
||||
return response.data.data;
|
||||
},
|
||||
send: async () => Promise.resolve(true),
|
||||
getSignData: async (id: string) => {
|
||||
const response = await network.client.post(USER_NOTIFICATONS_GENERATE_SIGN_DATA, {
|
||||
data: { data: id },
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
},
|
||||
getSignDataV2: async (id: string) => {
|
||||
const response = await network.client.post(USER_NOTIFICATONS_GENERATE_SIGN_DATA_V2, { data: [id] });
|
||||
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
export { signService };
|
||||
@@ -1,3 +1,5 @@
|
||||
import { LOCALIZATION } from './localization';
|
||||
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
enum NOTIFICATION_TYPE {
|
||||
STATEMENT = 'STATEMENT',
|
||||
@@ -19,4 +21,4 @@ const NOTIFICATION_TYPE_LABELS = {
|
||||
[NOTIFICATION_TYPE.LETTERS]: 'Письма',
|
||||
};
|
||||
|
||||
export { NOTIFICATION_TYPE_LABELS, NOTIFICATION_TYPE };
|
||||
export { NOTIFICATION_TYPE_LABELS, NOTIFICATION_TYPE, LOCALIZATION };
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
const LOCALIZATION = {
|
||||
TITLE: 'Новые уведомления',
|
||||
REQUIRED_READING: 'Обязательное к прочтению',
|
||||
GO_TO_NOTIFICATION_PAGE: ' Перейти в раздел',
|
||||
DATA_IS_FAILED: 'Данные не загрузились',
|
||||
ATTACHMENTS: 'Вложения',
|
||||
DOWNLOAD_ALL: 'Скачать все',
|
||||
ERROR_LOAD_ARCHIVE: 'Ошибка загрузки архива',
|
||||
ERROR_EXECUTE: 'Ошибка исполнения. Попробуйте еще раз',
|
||||
ERROR_LOAD_FILE: 'Ошибка загрузки файла',
|
||||
MARK_AS_READ: 'Отметить прочитанным',
|
||||
SIGNED: 'Подписано',
|
||||
READ: 'Прочитано',
|
||||
TAKEN_INTO_ACCOUNT: 'Принято к сведению',
|
||||
IMPORTANT: 'Важное',
|
||||
REQUIRING_EXECUTION: 'Требует исполнения',
|
||||
EXECUTED: 'Исполнено',
|
||||
LETTERS: 'Письма',
|
||||
EXECUTE: 'Исполнить',
|
||||
NOTIFICATION_IS_SIGNED: 'Прочтение уведомления успешно подписано',
|
||||
REQUIRED_NOTIFICATIONS: 'Обязательные уведомления',
|
||||
REQUIRED_NOTIFICATION: 'Обязательное уведомление',
|
||||
EXPAND_NOTIFICATION: 'Развернуть уведомление',
|
||||
COLLAPSE_NOTIFICATION: 'Свернуть уведомление',
|
||||
ALL_NOTIFICATIONS_IS_TAKEN_INTO_ACTION: 'Все уведомления приняты к сведению',
|
||||
};
|
||||
|
||||
export { LOCALIZATION };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './model';
|
||||
export * from './ui';
|
||||
export * from './constants';
|
||||
export * from './api';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { attachmentsZipName } from './attachment-zip-name';
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './useUserNotifications';
|
||||
export * from './useUserNotificationsLazyLoad';
|
||||
export * from './useDownloadFiles';
|
||||
export * from './openSignModal';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import type { ISignRequestData } from '@msb/crypto';
|
||||
import { openSignModal } from '@msb/crypto';
|
||||
import { type NotificationDto } from '@msb/http';
|
||||
|
||||
import { handleReachGoal, YM_GOALS } from '@msb/shared';
|
||||
import { signService } from '../api/sign';
|
||||
|
||||
function openNotificationSignModal(
|
||||
document: NotificationDto,
|
||||
readNotification: (notification: NotificationDto) => void,
|
||||
setSignModalOpened?: (value: boolean) => void
|
||||
) {
|
||||
return openSignModal({
|
||||
type: 'single',
|
||||
modalParams: {
|
||||
signToolView: { continueBtnText: 'Подписать' },
|
||||
noCertificatesModalProps: {
|
||||
toListText: 'Назад',
|
||||
},
|
||||
},
|
||||
onClose: () => {
|
||||
setSignModalOpened?.(false);
|
||||
},
|
||||
signData: {
|
||||
documentsRegistry: [
|
||||
{
|
||||
clientId: document.bankClient.clientId,
|
||||
documents: [{ id: document.id }],
|
||||
},
|
||||
],
|
||||
},
|
||||
api: {
|
||||
sign: async ([doc]: ISignRequestData[]) => {
|
||||
try {
|
||||
const { id: _id, ...docWithoutId } = doc;
|
||||
const data = await signService.sign(docWithoutId);
|
||||
|
||||
if (data) {
|
||||
readNotification(document);
|
||||
handleReachGoal(YM_GOALS.VIEW_ELEMENT, { [YM_GOALS.VIEW_ELEMENT]: { element_name: 'notification_signed' } });
|
||||
}
|
||||
|
||||
return [data];
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания документа: ', error);
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
getSignData: async ([id]: string[]) => {
|
||||
try {
|
||||
const resp = await signService.getSignDataV2(id);
|
||||
|
||||
return resp;
|
||||
} catch (error) {
|
||||
console.error('Ошибка генерации данных подписания: ', error);
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { openNotificationSignModal };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useSnackbar } from '@fractal-ui/overlays';
|
||||
import { download as downloadFile, useMutation, type NotificationDto } from '@msb/http';
|
||||
import { showFile } from '@msb/shared';
|
||||
import { createZip } from '../api';
|
||||
import { LOCALIZATION } from '../constants/localization';
|
||||
import { attachmentsZipName } from '../lib';
|
||||
|
||||
const useDownloadFiles = (notification?: NotificationDto, callback?: () => void) => {
|
||||
const { showSnackbarMessage } = useSnackbar();
|
||||
|
||||
const { mutateAsync: downloadAll, isLoading: downloadAllLoading } = useMutation({
|
||||
mutationFn: () => {
|
||||
const zipName = attachmentsZipName({
|
||||
organizationName: notification?.bankClient?.shortName,
|
||||
notificationId: notification?.id ?? '',
|
||||
notificationCreationDate: notification?.createdAt ?? '',
|
||||
});
|
||||
|
||||
return createZip(notification?.attachments.map(el => el.attachmentId) ?? [], zipName).then(response =>
|
||||
downloadFile(response.data.attachmentId, response.data.attachmentToken)
|
||||
);
|
||||
},
|
||||
onSuccess: (response: any) => {
|
||||
showFile(response.data, response.fileName, response.type);
|
||||
callback?.();
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_ARCHIVE });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
downloadAll,
|
||||
downloadAllLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export { useDownloadFiles };
|
||||
@@ -1,15 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
import { useRef } from 'react';
|
||||
import { useQuery } from '@msb/http';
|
||||
import type { CategoryCountDto, FiltersDto, NotificationDto } from '@msb/http';
|
||||
import { handleReachGoal, YM_GOALS } from '@msb/shared';
|
||||
import { type CategoryCountDto, FEATURE_TOGGLE_NAMES, type FiltersDto, type NotificationDto, useMutation, useQuery } from '@msb/http';
|
||||
import { BLOCKING_NOTIFICATION_MODAL_KEY, handleReachGoal, useFeatureToggles, YM_GOALS } from '@msb/shared';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
fetchUserNotificationsCategoryCount,
|
||||
fetchUserNotifications,
|
||||
fetchRequiredReadUserNotifications,
|
||||
fetchUserNotificationById,
|
||||
markNotificationAsRead,
|
||||
QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT,
|
||||
QUERY_KEY_USER_NOTIFICATION_LIST,
|
||||
QUERY_KEY_USER_NOTIFICATION_BY_ID,
|
||||
QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION,
|
||||
} from '../api';
|
||||
import { LOCALIZATION } from '../constants/localization';
|
||||
|
||||
@@ -70,12 +73,10 @@ const useUserNotifications = ({
|
||||
};
|
||||
|
||||
const useUserNotificationByid = (id: string) => {
|
||||
const { data, isLoading, isFetching, isSuccess, refetch, error } = useQuery<NotificationDto, Error | undefined>({
|
||||
const { data, isLoading, isFetching, isSuccess, refetch, error } = useQuery({
|
||||
queryKey: [QUERY_KEY_USER_NOTIFICATION_BY_ID, id],
|
||||
queryFn: () => fetchUserNotificationById(id),
|
||||
enabled: !!id,
|
||||
staleTime: 0,
|
||||
cacheTime: 0,
|
||||
retry: 3,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
@@ -90,4 +91,38 @@ const useUserNotificationByid = (id: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export { useUserNotificationCategoryCount, useUserNotifications, useUserNotificationByid };
|
||||
/** Пометить сообщение как прочитанное. */
|
||||
const useMarkAsRead = () =>
|
||||
useMutation({
|
||||
mutationFn: (id: string) => markNotificationAsRead(id),
|
||||
});
|
||||
|
||||
const useUserNotificationRequiredRead = (enabled: boolean) => {
|
||||
const { isEnabled } = useFeatureToggles(FEATURE_TOGGLE_NAMES.BLOCKING_NOTIFICATIONS);
|
||||
const location = useLocation();
|
||||
const { data, isLoading, isFetching, isSuccess, refetch, error } = useQuery<{ page: NotificationDto[]; size: number }, Error | undefined>(
|
||||
{
|
||||
queryKey: [QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION, location.pathname],
|
||||
queryFn: fetchRequiredReadUserNotifications,
|
||||
staleTime: 0,
|
||||
cacheTime: 0,
|
||||
enabled,
|
||||
onSettled: data => {
|
||||
if (data?.page && data?.page.length > 0 && isEnabled) {
|
||||
localStorage.setItem(BLOCKING_NOTIFICATION_MODAL_KEY, 'true');
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
notifications: data?.page || [],
|
||||
isLoading,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export { useUserNotificationCategoryCount, useUserNotificationByid, useUserNotificationRequiredRead, useMarkAsRead, useUserNotifications };
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { debounce } from '@fractal-ui/core';
|
||||
import type { NotificationCategory } from '@msb/http';
|
||||
@@ -115,7 +116,7 @@ const useNotificationsLazyLoad = <T>({ activeTab, filters }: { activeTab: T; fil
|
||||
})
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT] });
|
||||
await queryClient.invalidateQueries({ queryKey: [QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT] });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ButtonIcon, Skeleton } from '@fractal-ui/core';
|
||||
import type { IconComponent } from '@fractal-ui/library';
|
||||
import { FormatJpgIcon, FormatPngIcon, FormatPdfIcon, DocumentBlankIcon, EyeOpenIcon, DownloadIcon } from '@fractal-ui/library';
|
||||
import { useSnackbar } from '@fractal-ui/overlays';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
import { type NotificationAttachmentDto, download as downloadFile, useMutation, useQuery, useQueryClient } from '@msb/http';
|
||||
import { Flex, formatSize, showFile, previewFile, useDeviceType } from '@msb/shared';
|
||||
|
||||
import { LOCALIZATION } from '../../constants/localization';
|
||||
|
||||
import * as S from './styles';
|
||||
|
||||
import { QUERY_KEY_USER_NOTIFICATION_BY_ID } from '@/entities/UserNotification/api';
|
||||
|
||||
const MAXIMUM_PREVIEW_SIZE = 104_857_600; // 100MB
|
||||
|
||||
enum ATTACHMENT_FILE_TYPE {
|
||||
PDF = 'application/pdf',
|
||||
PNG = 'image/png',
|
||||
JPEG = 'image/jpeg',
|
||||
}
|
||||
|
||||
/** Получает иконку файла по его типу. */
|
||||
const getFileIcon = (contentType?: string): IconComponent => {
|
||||
switch (contentType) {
|
||||
case ATTACHMENT_FILE_TYPE.JPEG:
|
||||
return FormatJpgIcon;
|
||||
case ATTACHMENT_FILE_TYPE.PNG:
|
||||
return FormatPngIcon;
|
||||
case ATTACHMENT_FILE_TYPE.PDF:
|
||||
return FormatPdfIcon;
|
||||
default:
|
||||
return DocumentBlankIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const responseToObjectURL = (response: any) => {
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
/** Компонент для показа изображения из file-storage. */
|
||||
const AttachmentPicture = ({ attachmentId, attachmentToken }: Pick<NotificationAttachmentDto, 'attachmentId' | 'attachmentToken'>) => {
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['attachment-image', attachmentId],
|
||||
queryFn: () => downloadFile(attachmentId, attachmentToken),
|
||||
enabled: Boolean(attachmentId && attachmentToken),
|
||||
select: responseToObjectURL,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return <Skeleton dataName={`loading-attachment-${attachmentId}`} height="160px" mb="3" variant="rounded" width="100%" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <S.Image height="160px" mb="3" src={data} />;
|
||||
}, [attachmentId, data, error, isLoading]);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const Attachment = ({ attachment, notificationId }: { attachment: NotificationAttachmentDto; notificationId: string }) => {
|
||||
const FileIcon = useMemo(() => getFileIcon(attachment.contentType), [attachment]);
|
||||
const { isTouchDevice } = useDeviceType();
|
||||
const { showSnackbarMessage } = useSnackbar();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const onMouseEnter = () => setIsHovered(true);
|
||||
const onMouseLeave = () => setIsHovered(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateMessage = () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, notificationId]);
|
||||
};
|
||||
|
||||
const isPreviewAvailable =
|
||||
!isTouchDevice &&
|
||||
[ATTACHMENT_FILE_TYPE.PDF, ATTACHMENT_FILE_TYPE.PNG, ATTACHMENT_FILE_TYPE.JPEG].includes(
|
||||
attachment.contentType as ATTACHMENT_FILE_TYPE
|
||||
) &&
|
||||
attachment.dataSize <= MAXIMUM_PREVIEW_SIZE;
|
||||
|
||||
const { mutateAsync: preview, isLoading: isPreviewLoading } = useMutation({
|
||||
mutationFn: () => downloadFile(attachment.attachmentId, attachment.attachmentToken),
|
||||
onSuccess: response => {
|
||||
previewFile(response.data, response.type);
|
||||
invalidateMessage();
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_FILE });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: download, isLoading: isDownloadLoading } = useMutation({
|
||||
mutationFn: () => downloadFile(attachment.attachmentId, attachment.attachmentToken),
|
||||
onSuccess: response => {
|
||||
showFile(response.data, response.fileName, response.type);
|
||||
invalidateMessage();
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_FILE });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<S.AttachmentContainer onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex row>
|
||||
<S.AttachmentPictogram row alignItems="center" justifyContent="center">
|
||||
<FileIcon />
|
||||
</S.AttachmentPictogram>
|
||||
<Flex column ml="3" mr="5">
|
||||
<S.FileName>{attachment.fileName}</S.FileName>
|
||||
<Text.P4 color="text.secondary">{formatSize(attachment.dataSize)}</Text.P4>
|
||||
</Flex>
|
||||
<Flex row columnGap="2" ml="auto" opacity={isHovered || isTouchDevice ? 1 : 0}>
|
||||
{isPreviewAvailable && (
|
||||
<ButtonIcon
|
||||
color="text.secondary"
|
||||
dataAction="preview-attachment"
|
||||
icon={EyeOpenIcon}
|
||||
isLoading={isPreviewLoading}
|
||||
size="S"
|
||||
title="Посмотреть"
|
||||
variant="ghost"
|
||||
onClick={() => preview()}
|
||||
/>
|
||||
)}
|
||||
<ButtonIcon
|
||||
color="text.secondary"
|
||||
dataAction="download-attachment"
|
||||
icon={DownloadIcon}
|
||||
isLoading={isDownloadLoading}
|
||||
size="S"
|
||||
title="Скачать"
|
||||
variant="ghost"
|
||||
onClick={() => download()}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</S.AttachmentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { Attachment, AttachmentPicture };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Attachement';
|
||||
@@ -0,0 +1,42 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
import { Flex } from '@msb/shared';
|
||||
import { layout, space } from 'styled-system';
|
||||
|
||||
const AttachmentContainer = styled.div`
|
||||
padding: 16px;
|
||||
background-color: #f9f9fb;
|
||||
border-radius: 12px;
|
||||
`;
|
||||
|
||||
const FileName = styled(Text.P2)`
|
||||
max-width: 275px;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const Image = styled.img(
|
||||
{
|
||||
display: 'block',
|
||||
height: '160px',
|
||||
width: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
layout,
|
||||
space
|
||||
);
|
||||
|
||||
const AttachmentPictogram = styled(Flex)`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
background: linear-gradient(225deg, #f0f6ff 0%, #d4e1fa 100%);
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #1e222e8f;
|
||||
}
|
||||
`;
|
||||
|
||||
export { Image, AttachmentContainer, FileName, AttachmentPictogram };
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import type { ButtonProps } from '@fractal-ui/core';
|
||||
import { Button, ButtonLink, Spinner } from '@fractal-ui/core';
|
||||
import { Badge } from '@fractal-ui/extended';
|
||||
import { DoneIcon } from '@fractal-ui/library';
|
||||
import { Text, Title } from '@fractal-ui/styling';
|
||||
import type { NotificationDto, UseMutateFunction } from '@msb/http';
|
||||
import { useQueryClient, READ_STATUS } from '@msb/http';
|
||||
import { DataIsFailedError, Flex, handleReachGoal, MEDIA, useMediaQuery, YaMetrikaReachGoal, YM_GOALS } from '@msb/shared';
|
||||
import dayjs from 'dayjs';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentPicture,
|
||||
useDownloadFiles,
|
||||
useUserNotificationByid,
|
||||
openNotificationSignModal,
|
||||
LOCALIZATION,
|
||||
QUERY_KEY_USER_NOTIFICATION_BY_ID,
|
||||
QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT,
|
||||
QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION,
|
||||
} from '@/entities/UserNotification';
|
||||
import { Loader } from '@/shared/ui/components';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
setSignModalOpened(value: boolean): void;
|
||||
markAsRead: UseMutateFunction<any, unknown, string>;
|
||||
markAsReadLoading: boolean;
|
||||
}
|
||||
|
||||
const BlockingNotification = ({ id, setSignModalOpened, markAsRead, markAsReadLoading }: Props) => {
|
||||
const isMobile = useMediaQuery(MEDIA.mobile);
|
||||
const location = useLocation();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { notification, isLoading, isFetching, error, refetch } = useUserNotificationByid(id);
|
||||
|
||||
const { downloadAll, downloadAllLoading } = useDownloadFiles(notification, () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
});
|
||||
const isSigned = notification?.signatures && notification.signatures.length > 0;
|
||||
|
||||
const isSignable = notification?.isRequiredSing && notification.bankClient;
|
||||
|
||||
const isReadButtonVisible = notification?.isRequiredRead && notification.status === READ_STATUS.NEW;
|
||||
|
||||
const isTakenIntoAccount = notification?.isRequiredRead && notification.status === READ_STATUS.READ;
|
||||
|
||||
const updateNotificationStatus = useCallback(() => {
|
||||
if (notification) {
|
||||
markAsRead(notification.id, {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
await Promise.resolve(
|
||||
queryClient.setQueryData(
|
||||
[QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION, location.pathname],
|
||||
(oldData?: { page: NotificationDto[]; size: number }) => {
|
||||
if (!oldData || !oldData.page) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
const updatedPage = oldData.page.map(item => (notification.id === item.id ? { ...item, status: READ_STATUS.READ } : item));
|
||||
|
||||
return { ...oldData, page: updatedPage };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [notification, markAsRead, queryClient, id, location.pathname]);
|
||||
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleReachGoal(YM_GOALS.BUTTON_CLICK, { [YM_GOALS.BUTTON_CLICK]: { element_name: 'mark_as_read' } });
|
||||
|
||||
if (isSignable) {
|
||||
setSignModalOpened(true);
|
||||
openNotificationSignModal(notification, updateNotificationStatus, setSignModalOpened);
|
||||
} else {
|
||||
updateNotificationStatus();
|
||||
}
|
||||
}, [isSignable, notification, setSignModalOpened, updateNotificationStatus]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (isLoading && id) {
|
||||
return <Loader dataName="loader" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<YaMetrikaReachGoal goalType={YM_GOALS.ERROR} params={{ [YM_GOALS.ERROR]: { error_description: LOCALIZATION.DATA_IS_FAILED } }} />
|
||||
<DataIsFailedError
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
notification && (
|
||||
<>
|
||||
<Badge size="S" type={isTakenIntoAccount ? 'success' : 'error'}>
|
||||
{isTakenIntoAccount ? LOCALIZATION.TAKEN_INTO_ACCOUNT : LOCALIZATION.REQUIRED_READING}
|
||||
</Badge>
|
||||
<Flex column marginTop="20px" wordBreak="break-word">
|
||||
{notification.bankClient?.shortName && (
|
||||
<Text.P3 color="text.secondary" marginBottom="4px">
|
||||
{notification.bankClient.shortName}
|
||||
</Text.P3>
|
||||
)}
|
||||
{notification.createdAt && (
|
||||
<Text.P3 color="text.secondary" marginBottom="16px">
|
||||
{dayjs(notification.createdAt).format('DD.MM.YYYY, HH:mm')}
|
||||
</Text.P3>
|
||||
)}
|
||||
{notification.type.notificationHeader && <Title.H4 marginBottom="12px">{notification.type.notificationHeader}</Title.H4>}
|
||||
{notification.picture && (
|
||||
<AttachmentPicture attachmentId={notification.picture.attachmentId} attachmentToken={notification.picture.attachmentToken} />
|
||||
)}
|
||||
{notification.body && (
|
||||
<Text.P2
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: notification.body,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{notification.attachments.length > 0 && (
|
||||
<div>
|
||||
<Flex row justifyContent="space-between" marginBottom="12px">
|
||||
<Flex row gap={2}>
|
||||
<Title.H4>{LOCALIZATION.ATTACHMENTS}</Title.H4>
|
||||
<Badge size="XS" type="system">
|
||||
{notification.attachments.length}
|
||||
</Badge>
|
||||
</Flex>
|
||||
{!isMobile && (
|
||||
<ButtonLink
|
||||
dataAction="download-all"
|
||||
size="S"
|
||||
onClick={() => {
|
||||
downloadAll();
|
||||
}}
|
||||
>
|
||||
{downloadAllLoading ? <Spinner dataName="download-all-loader" size="S" /> : LOCALIZATION.DOWNLOAD_ALL}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex column gap={3}>
|
||||
{notification.attachments.map(attachment => (
|
||||
<Attachment key={attachment.attachmentId} attachment={attachment} notificationId={notification.id} />
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}, [isLoading, id, error, notification, isTakenIntoAccount, isMobile, downloadAllLoading, refetch, downloadAll]);
|
||||
|
||||
const isFooterShown = useMemo(
|
||||
() =>
|
||||
[notification?.isRequiredRead && notification.status === READ_STATUS.READ && !isSigned, isSigned, isReadButtonVisible].some(
|
||||
option => option
|
||||
),
|
||||
[notification, isSigned, isReadButtonVisible]
|
||||
);
|
||||
|
||||
const actions: ButtonProps[] = useMemo(() => {
|
||||
const secondaryButtons: ButtonProps[] = [];
|
||||
const primaryButtons: ButtonProps[] = [];
|
||||
|
||||
if (!isFooterShown || !notification) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isReadButtonVisible) {
|
||||
primaryButtons.push({
|
||||
title: LOCALIZATION.MARK_AS_READ,
|
||||
dataAction: 'read',
|
||||
onClick: handleMarkAsRead,
|
||||
disabled: isFetching,
|
||||
isLoading: markAsReadLoading,
|
||||
width: 'auto',
|
||||
variant: 'primary',
|
||||
});
|
||||
}
|
||||
|
||||
if (isSigned) {
|
||||
secondaryButtons.push({
|
||||
title: LOCALIZATION.SIGNED,
|
||||
dataAction: 'signed',
|
||||
icon: DoneIcon,
|
||||
disabled: true,
|
||||
width: 'auto',
|
||||
variant: 'secondary',
|
||||
});
|
||||
} else if (notification.isRequiredRead && notification.status === READ_STATUS.READ && !isSigned) {
|
||||
secondaryButtons.push({
|
||||
title: LOCALIZATION.READ,
|
||||
dataAction: 'read',
|
||||
icon: DoneIcon,
|
||||
disabled: true,
|
||||
width: 'auto',
|
||||
variant: 'secondary',
|
||||
});
|
||||
}
|
||||
|
||||
return [...secondaryButtons, ...primaryButtons];
|
||||
}, [isFooterShown, notification, isReadButtonVisible, isSigned, handleMarkAsRead, markAsReadLoading, isFetching]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{content}
|
||||
<Flex row gap={2} justifyContent="flex-end" marginTop="20px" width="100%">
|
||||
{actions.map(action => (
|
||||
<Button key={action.title} size="S" {...action}>
|
||||
{action.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlockingNotification };
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import { type ReactElement, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { ButtonIcon } from '@fractal-ui/core';
|
||||
import { AttentionIcon, UpIcon, OkIcon } from '@fractal-ui/library';
|
||||
import { Modal, useSnackbar } from '@fractal-ui/overlays';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
import type { NotificationDto } from '@msb/http';
|
||||
import { READ_STATUS, useQueryClient } from '@msb/http';
|
||||
import {
|
||||
Flex,
|
||||
MEDIA,
|
||||
PATHS_PROFILE,
|
||||
PATHS_NOTIFICATIONS,
|
||||
removeCmpFromPortal,
|
||||
SmoothAutoHeight,
|
||||
useDeviceType,
|
||||
useMediaQuery,
|
||||
BLOCKING_NOTIFICATION_MODAL_KEY,
|
||||
handleReachGoal,
|
||||
YM_GOALS,
|
||||
} from '@msb/shared';
|
||||
import dayjs from 'dayjs';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useMarkAsRead } from '../../model';
|
||||
import { BlockingNotification } from './BlockingNotification';
|
||||
import * as S from './styles';
|
||||
import { QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION } from '@/entities/UserNotification/api';
|
||||
import { LOCALIZATION } from '@/entities/UserNotification/constants';
|
||||
|
||||
const DELAY_MS = 1000;
|
||||
const excludePaths = [PATHS_NOTIFICATIONS.NOTIFICATIONS, PATHS_PROFILE.ORGANIZATIONS];
|
||||
|
||||
interface Props {
|
||||
notifications: NotificationDto[];
|
||||
}
|
||||
|
||||
const BlockingNotificationModal = ({ notifications }: Props): ReactElement => {
|
||||
const collapsedRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [isSignModalOpened, setIsSignModalOpened] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
const { showSnackbarMessage } = useSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
const isMobile = useMediaQuery(MEDIA.mobile);
|
||||
const { isTouchDevice } = useDeviceType();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [shouldShowModal, setShouldShowModal] = useState(false);
|
||||
|
||||
const { mutate: markAsRead, isLoading: markAsReadLoading } = useMarkAsRead();
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShow = notifications.length > 0 && !excludePaths.some(path => location.pathname.includes(path));
|
||||
|
||||
setShouldShowModal(shouldShow);
|
||||
|
||||
if (shouldShow && !isModalOpen) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsModalOpen(true);
|
||||
handleReachGoal(YM_GOALS.VIEW_ELEMENT, { [YM_GOALS.VIEW_ELEMENT]: { element_name: 'blocking_modal' } });
|
||||
}, DELAY_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (notifications.length > 0 && notifications.every(notification => notification.status === READ_STATUS.READ)) {
|
||||
queryClient.invalidateQueries([QUERY_KEY_REQUIRED_READ_USER_NOTIFICATION]).then(() => {
|
||||
setIsModalOpen(false);
|
||||
showSnackbarMessage({ type: 'success', icon: OkIcon, message: LOCALIZATION.ALL_NOTIFICATIONS_IS_TAKEN_INTO_ACTION });
|
||||
localStorage.removeItem(BLOCKING_NOTIFICATION_MODAL_KEY);
|
||||
});
|
||||
}
|
||||
}, [location.pathname, notifications, isModalOpen, queryClient, showSnackbarMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname.includes(PATHS_PROFILE.ORGANIZATIONS) && isSignModalOpened) {
|
||||
removeCmpFromPortal('SignModal');
|
||||
setIsSignModalOpened(false);
|
||||
}
|
||||
}, [location.pathname, isSignModalOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notifications.length === 1) {
|
||||
setExpandedId(notifications[0].id);
|
||||
}
|
||||
}, [notifications]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setIsModalOpen(false);
|
||||
setShouldShowModal(false);
|
||||
}, []);
|
||||
|
||||
const handleExpandCard = useCallback((id: string) => {
|
||||
handleReachGoal(YM_GOALS.BUTTON_CLICK, { [YM_GOALS.BUTTON_CLICK]: { element_name: 'opening_a_notification' } });
|
||||
setExpandedId(prevExpandedId => (prevExpandedId === id ? null : id));
|
||||
}, []);
|
||||
|
||||
const setSignModalOpened = (value: boolean) => {
|
||||
setIsSignModalOpened(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideBackIcon
|
||||
preventCloseOnOutside
|
||||
preventSimpleClose
|
||||
header={notifications.length > 1 ? LOCALIZATION.REQUIRED_NOTIFICATIONS : LOCALIZATION.REQUIRED_NOTIFICATION}
|
||||
isNeedScroll={false}
|
||||
isOpen={shouldShowModal && isModalOpen && !isSignModalOpened}
|
||||
minHeight={638}
|
||||
width={768}
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<Flex
|
||||
ref={node => {
|
||||
if (node) {
|
||||
const parent = node.parentElement;
|
||||
|
||||
if (parent) {
|
||||
parent.style.marginBottom = '0px';
|
||||
}
|
||||
}
|
||||
}}
|
||||
column
|
||||
gap="16px"
|
||||
marginTop="20px"
|
||||
>
|
||||
{notifications.map(notification => {
|
||||
const isExpanded = notifications.length === 1 ? true : expandedId === notification.id;
|
||||
|
||||
return (
|
||||
<S.Card key={notification.id} column backgroundColor="bg.four">
|
||||
<Flex row alignItems="flex-start" gap="12px">
|
||||
<S.Pictogram
|
||||
row
|
||||
alignItems="center"
|
||||
bg={`control.multicolored.${notification.status === READ_STATUS.READ ? 'bgGreen' : 'bgOrange'}`}
|
||||
justifyContent="center"
|
||||
transition="background-color 0.3s ease"
|
||||
>
|
||||
{notification.status === READ_STATUS.READ ? (
|
||||
<OkIcon color="control.multicolored.textGreen" size="L" />
|
||||
) : (
|
||||
<AttentionIcon color="control.multicolored.textOrange" size="L" />
|
||||
)}
|
||||
</S.Pictogram>
|
||||
|
||||
<Flex
|
||||
column
|
||||
flex={1}
|
||||
onClick={() => {
|
||||
if (isTouchDevice) {
|
||||
handleExpandCard(notification.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text.P1 flex={1} fontSize={`typography.P${isMobile ? 2 : 1}.fontSize.S`} wordBreak="break-word">
|
||||
{notification.type.notificationHeader}
|
||||
</Text.P1>
|
||||
|
||||
{notifications.length > 1 && !isTouchDevice && (
|
||||
<S.ExpandButtonWrapper $isCollapsed={!isExpanded}>
|
||||
<ButtonIcon
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={isExpanded ? LOCALIZATION.COLLAPSE_NOTIFICATION : LOCALIZATION.EXPAND_NOTIFICATION}
|
||||
dataAction={`toggle-notification-${notification.id}`}
|
||||
icon={UpIcon}
|
||||
size="S"
|
||||
variant="ghost"
|
||||
onClick={() => handleExpandCard(notification.id)}
|
||||
/>
|
||||
</S.ExpandButtonWrapper>
|
||||
)}
|
||||
|
||||
<Text.P2 color="text.secondary" fontSize={`typography.P${isMobile ? 3 : 2}.fontSize.S`}>
|
||||
{dayjs(notification.createdAt).format('DD.MM.YYYY, HH:mm')}
|
||||
</Text.P2>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<SmoothAutoHeight key={`accordion-${notification.id}`} isAccordionMode elementRef={collapsedRef} isCollapsed={!isExpanded}>
|
||||
<S.CollapsedArea ref={collapsedRef}>
|
||||
<BlockingNotification
|
||||
id={notification.id}
|
||||
markAsRead={markAsRead}
|
||||
markAsReadLoading={markAsReadLoading}
|
||||
setSignModalOpened={setSignModalOpened}
|
||||
/>
|
||||
</S.CollapsedArea>
|
||||
</SmoothAutoHeight>
|
||||
</S.Card>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlockingNotificationModal };
|
||||
@@ -0,0 +1 @@
|
||||
export { BlockingNotificationModal } from './BlockingNotificationModal';
|
||||
@@ -0,0 +1,38 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { DEFAULT_ANIMATION_DURATION_MS, Flex } from '@msb/shared';
|
||||
|
||||
const CollapsedArea = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 138px;
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
|
||||
${({ theme }) => css`
|
||||
background-color: ${theme.colors.bg.primary};
|
||||
`}
|
||||
`;
|
||||
|
||||
const Pictogram = styled(Flex)`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const Card = styled(Flex)({ padding: '20px', borderRadius: '12px', position: 'relative' });
|
||||
|
||||
const ExpandButtonWrapper = styled.div<{ $isCollapsed: boolean }>(({ $isCollapsed }) => ({
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
svg: {
|
||||
transition: `transform ${DEFAULT_ANIMATION_DURATION_MS}ms ease`,
|
||||
transform: $isCollapsed ? 'rotate(180deg)' : 'rotate(0)',
|
||||
},
|
||||
}));
|
||||
|
||||
export { ExpandButtonWrapper, CollapsedArea, Card, Pictogram };
|
||||
@@ -1 +1,3 @@
|
||||
export { UserNotifications } from './UserNotifications';
|
||||
export * from './Attachment';
|
||||
export * from './BlockingNotification';
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import { type ReactElement } from 'react';
|
||||
import type { RemoteRouteConfigDto } from '@msb/http';
|
||||
// import { IS_WELCOME_MESSAGE_READ_KEY, useModal } from '@msb/shared';
|
||||
import { useEffect, type ReactElement } from 'react';
|
||||
import { FEATURE_TOGGLE_NAMES, type RemoteRouteConfigDto } from '@msb/http';
|
||||
import { IS_WELCOME_MESSAGE_READ_KEY, BLOCKING_NOTIFICATION_MODAL_KEY, useModal, useAppContext, useFeatureToggles } from '@msb/shared';
|
||||
import * as S from './Layout.styles';
|
||||
import LayoutContent from './LayoutContent';
|
||||
import { StyledThemeProvider } from '@/app/providers';
|
||||
// import { WelcomeMessageModal } from '@/shared/ui/components';
|
||||
import { BlockingNotificationModal } from '@/entities/UserNotification/ui/BlockingNotification';
|
||||
import { WelcomeMessageModal } from '@/shared/ui/components';
|
||||
|
||||
interface Props {
|
||||
modules: RemoteRouteConfigDto[] | undefined;
|
||||
}
|
||||
|
||||
const Layout = ({ modules }: Props): ReactElement => (
|
||||
// const { showModal, isReady } = useModal(WelcomeMessageModal);
|
||||
const Layout = ({ modules }: Props): ReactElement => {
|
||||
const { requiredNotifications, isRequiredNotificationsLoaded, isRequiredNotificationsFailed } = useAppContext();
|
||||
const { isEnabled } = useFeatureToggles(FEATURE_TOGGLE_NAMES.BLOCKING_NOTIFICATIONS);
|
||||
const { showModal, isReady } = useModal(WelcomeMessageModal);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isReady && !localStorage.getItem(IS_WELCOME_MESSAGE_READ_KEY)) {
|
||||
// showModal({ onClose: () => localStorage.setItem(IS_WELCOME_MESSAGE_READ_KEY, 'true') });
|
||||
// }
|
||||
// }, [isReady]);
|
||||
useEffect(() => {
|
||||
if (
|
||||
isReady &&
|
||||
!localStorage.getItem(BLOCKING_NOTIFICATION_MODAL_KEY) &&
|
||||
!localStorage.getItem(IS_WELCOME_MESSAGE_READ_KEY) &&
|
||||
(isRequiredNotificationsLoaded || isRequiredNotificationsFailed)
|
||||
) {
|
||||
showModal({ onClose: () => localStorage.setItem(IS_WELCOME_MESSAGE_READ_KEY, 'true') });
|
||||
}
|
||||
}, [isRequiredNotificationsLoaded, isReady, isRequiredNotificationsFailed, showModal]);
|
||||
|
||||
<StyledThemeProvider>
|
||||
<S.GlobalStyles />
|
||||
<LayoutContent modules={modules} />
|
||||
</StyledThemeProvider>
|
||||
);
|
||||
return (
|
||||
<StyledThemeProvider>
|
||||
<S.GlobalStyles />
|
||||
<LayoutContent modules={modules} />
|
||||
{isEnabled && <BlockingNotificationModal notifications={requiredNotifications} />}
|
||||
</StyledThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const LOCALIZATION = {
|
||||
IMPORTANT: 'Важное',
|
||||
REQUIRED_READING: 'Обязательное к прочтению',
|
||||
REQUIRING_EXECUTION: 'Требует исполнения',
|
||||
EXECUTED: 'Исполнено',
|
||||
LETTERS: 'Письма',
|
||||
ATTACHMENTS: 'Вложения',
|
||||
DOWNLOAD_ALL: 'Скачать все',
|
||||
EXECUTE: 'Исполнить',
|
||||
ERROR_LOAD_ARCHIVE: 'Ошибка загрузки архива',
|
||||
ERROR_EXECUTE: 'Ошибка исполнения. Попробуйте еще раз',
|
||||
ERROR_LOAD_FILE: 'Ошибка загрузки файла',
|
||||
};
|
||||
|
||||
export { LOCALIZATION };
|
||||
@@ -1 +0,0 @@
|
||||
export * from './attachment-zip-name';
|
||||
@@ -1,48 +1,31 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { ButtonIcon, ButtonLink, Skeleton, Spinner } from '@fractal-ui/core';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
import { ButtonLink, Spinner } from '@fractal-ui/core';
|
||||
import { Badge } from '@fractal-ui/extended';
|
||||
import type { IconComponent } from '@fractal-ui/library';
|
||||
import { FormatJpgIcon, FormatPngIcon, FormatPdfIcon, DocumentBlankIcon, EyeOpenIcon, DownloadIcon } from '@fractal-ui/library';
|
||||
import { DoneIcon } from '@fractal-ui/library';
|
||||
import { Drawer, useSnackbar } from '@fractal-ui/overlays';
|
||||
import type { ModalButtonProps } from '@fractal-ui/overlays';
|
||||
import { Text, Title } from '@fractal-ui/styling';
|
||||
import {
|
||||
type NotificationDto,
|
||||
type NotificationAttachmentDto,
|
||||
download as downloadFile,
|
||||
useMutation,
|
||||
useQuery,
|
||||
EXECUTE_STATUS,
|
||||
useQueryClient,
|
||||
} from '@msb/http';
|
||||
import {
|
||||
DataIsFailedError,
|
||||
Flex,
|
||||
formatSize,
|
||||
MEDIA,
|
||||
PATHS,
|
||||
showFile,
|
||||
previewFile,
|
||||
useMediaQuery,
|
||||
useRedirect,
|
||||
EXTERNAL_URL_REGEXP,
|
||||
} from '@msb/shared';
|
||||
import { type NotificationDto, useMutation, EXECUTE_STATUS, useQueryClient, READ_STATUS } from '@msb/http';
|
||||
import { DataIsFailedError, Flex, MEDIA, PATHS, useMediaQuery, useRedirect, EXTERNAL_URL_REGEXP, useDeviceType } from '@msb/shared';
|
||||
import dayjs from 'dayjs';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { LOCALIZATION } from '../constants/localization';
|
||||
import { attachmentsZipName } from '../lib';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import * as S from './UserNotificationsPage.styles';
|
||||
import { useUserNotificationByid } from '@/entities/UserNotification';
|
||||
import {
|
||||
createZip,
|
||||
Attachment,
|
||||
AttachmentPicture,
|
||||
useDownloadFiles,
|
||||
useUserNotificationByid,
|
||||
openNotificationSignModal,
|
||||
} from '@/entities/UserNotification';
|
||||
import {
|
||||
executeNotification,
|
||||
QUERY_KEY_USER_NOTIFICATION_BY_ID,
|
||||
QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT,
|
||||
} from '@/entities/UserNotification/api';
|
||||
import { icons } from '@/entities/UserNotification/assets';
|
||||
import { NOTIFICATION_TYPE_LABELS } from '@/entities/UserNotification/constants';
|
||||
import { NOTIFICATION_TYPE_LABELS, LOCALIZATION } from '@/entities/UserNotification/constants';
|
||||
import { Loader } from '@/shared/ui/components';
|
||||
|
||||
interface Props {
|
||||
@@ -55,161 +38,37 @@ interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const MAXIMUM_PREVIEW_SIZE = 104_857_600; // 100MB
|
||||
|
||||
enum ATTACHMENT_FILE_TYPE {
|
||||
PDF = 'application/pdf',
|
||||
PNG = 'image/png',
|
||||
JPEG = 'image/jpeg',
|
||||
}
|
||||
|
||||
/** Получает иконку файла по его типу. */
|
||||
const getFileIcon = (contentType?: string): IconComponent => {
|
||||
switch (contentType) {
|
||||
case ATTACHMENT_FILE_TYPE.JPEG:
|
||||
return FormatJpgIcon;
|
||||
case ATTACHMENT_FILE_TYPE.PNG:
|
||||
return FormatPngIcon;
|
||||
case ATTACHMENT_FILE_TYPE.PDF:
|
||||
return FormatPdfIcon;
|
||||
default:
|
||||
return DocumentBlankIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const responseToObjectURL = (response: any) => {
|
||||
if (!response.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
/** Компонент для показа изображения из file-storage. */
|
||||
export const AttachmentPicture = ({
|
||||
attachmentId,
|
||||
attachmentToken,
|
||||
}: Pick<NotificationAttachmentDto, 'attachmentId' | 'attachmentToken'>) => {
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ['attachment-image', attachmentId],
|
||||
queryFn: () => downloadFile(attachmentId, attachmentToken),
|
||||
enabled: Boolean(attachmentId && attachmentToken),
|
||||
select: responseToObjectURL,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (isLoading) {
|
||||
return <Skeleton dataName={`loading-attachment-${attachmentId}`} height="160px" mb="3" variant="rounded" width="100%" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <S.Image height="160px" mb="3" src={data} />;
|
||||
}, [attachmentId, data, error, isLoading]);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const Attachment = ({ attachment, notificationId }: { attachment: NotificationAttachmentDto; notificationId: string }) => {
|
||||
const FileIcon = useMemo(() => getFileIcon(attachment.contentType), [attachment]);
|
||||
const isMobile = useMediaQuery(MEDIA.mobile);
|
||||
const { showSnackbarMessage } = useSnackbar();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const onMouseEnter = () => setIsHovered(true);
|
||||
const onMouseLeave = () => setIsHovered(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateMessage = () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, notificationId]);
|
||||
};
|
||||
|
||||
const isPreviewAvailable =
|
||||
!isMobile &&
|
||||
[ATTACHMENT_FILE_TYPE.PDF, ATTACHMENT_FILE_TYPE.PNG, ATTACHMENT_FILE_TYPE.JPEG].includes(
|
||||
attachment.contentType as ATTACHMENT_FILE_TYPE
|
||||
) &&
|
||||
attachment.dataSize <= MAXIMUM_PREVIEW_SIZE;
|
||||
|
||||
const { mutateAsync: preview, isLoading: isPreviewLoading } = useMutation({
|
||||
mutationFn: () => downloadFile(attachment.attachmentId, attachment.attachmentToken),
|
||||
onSuccess: response => {
|
||||
previewFile(response.data, response.type);
|
||||
invalidateMessage();
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_FILE });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: download, isLoading: isDownloadLoading } = useMutation({
|
||||
mutationFn: () => downloadFile(attachment.attachmentId, attachment.attachmentToken),
|
||||
onSuccess: response => {
|
||||
showFile(response.data, response.fileName, response.type);
|
||||
invalidateMessage();
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_FILE });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<S.AttachmentContainer onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex row>
|
||||
<S.AttachmentPictogram row alignItems="center" justifyContent="center">
|
||||
<FileIcon />
|
||||
</S.AttachmentPictogram>
|
||||
<Flex column ml="3" mr="5">
|
||||
<S.FileName>{attachment.fileName}</S.FileName>
|
||||
<Text.P4 color="text.secondary">{formatSize(attachment.dataSize)}</Text.P4>
|
||||
</Flex>
|
||||
<Flex row columnGap="2" ml="auto" opacity={isHovered || isMobile ? 1 : 0}>
|
||||
{isPreviewAvailable && (
|
||||
<ButtonIcon
|
||||
color="text.secondary"
|
||||
dataAction="preview-attachment"
|
||||
icon={EyeOpenIcon}
|
||||
isLoading={isPreviewLoading}
|
||||
size="S"
|
||||
title="Посмотреть"
|
||||
variant="ghost"
|
||||
onClick={() => preview()}
|
||||
/>
|
||||
)}
|
||||
<ButtonIcon
|
||||
color="text.secondary"
|
||||
dataAction="download-attachment"
|
||||
icon={DownloadIcon}
|
||||
isLoading={isDownloadLoading}
|
||||
size="S"
|
||||
title="Скачать"
|
||||
variant="ghost"
|
||||
onClick={() => download()}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</S.AttachmentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationPageDrawer = ({ setBadge, readNotification, setNotificationExecutedStatus }: Props) => {
|
||||
const { id } = useParams<Params>();
|
||||
const { push } = useHistory();
|
||||
const isMobile = useMediaQuery(MEDIA.mobile);
|
||||
const { showSnackbarMessage } = useSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const goToNotificationsList = useRedirect(PATHS.NOTIFICATIONS);
|
||||
|
||||
const { notification, isLoading, error, refetch } = useUserNotificationByid(id);
|
||||
const { isTouchDevice } = useDeviceType();
|
||||
|
||||
const targetOpen = isTouchDevice ? '_self' : '_blank';
|
||||
|
||||
const invalidateQueries = useCallback(() => {
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT]);
|
||||
}, [id, queryClient]);
|
||||
|
||||
const { notification, isLoading, error, isSuccess, refetch } = useUserNotificationByid(id);
|
||||
const { downloadAll, downloadAllLoading } = useDownloadFiles(notification, () => {
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
});
|
||||
const isSigned = notification?.signatures && notification.signatures.length > 0;
|
||||
|
||||
const isSignable = notification?.isRequiredSing && notification.bankClient;
|
||||
|
||||
const isReadButtonVisible = notification?.isRequiredRead && notification.status === READ_STATUS.NEW;
|
||||
|
||||
const isExecuteButtonVisible =
|
||||
notification?.actionUrl &&
|
||||
notification &&
|
||||
!notification.isBlocked &&
|
||||
notification.actionUrl &&
|
||||
(!notification.executionDeadline || dayjs(notification.executionDeadline).isSameOrAfter(dayjs())) &&
|
||||
notification.executionStatus !== EXECUTE_STATUS.EXECUTED;
|
||||
|
||||
@@ -224,15 +83,12 @@ const NotificationPageDrawer = ({ setBadge, readNotification, setNotificationExe
|
||||
setNotificationExecutedStatus(id);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_CATEGORY_COUNT]);
|
||||
invalidateQueries();
|
||||
|
||||
if (notification.actionUrl?.includes('letter') && !EXTERNAL_URL_REGEXP.test(notification.actionUrl)) {
|
||||
window.open(`${process.env.ECO_CLIENT_ENDPOINT ?? ''}${notification.actionUrl}`, '_self');
|
||||
if (notification.actionUrl && !EXTERNAL_URL_REGEXP.test(notification.actionUrl)) {
|
||||
window.open(`${process.env.ECO_CLIENT_ENDPOINT ?? ''}${notification.actionUrl}`, targetOpen);
|
||||
} else if (notification.actionUrl && EXTERNAL_URL_REGEXP.test(notification.actionUrl)) {
|
||||
window.open(notification.actionUrl, '_self');
|
||||
} else if (notification.actionUrl) {
|
||||
push(notification.actionUrl);
|
||||
window.open(notification.actionUrl, targetOpen);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
@@ -240,36 +96,29 @@ const NotificationPageDrawer = ({ setBadge, readNotification, setNotificationExe
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: downloadAll, isLoading: downloadAllLoading } = useMutation({
|
||||
mutationFn: () => {
|
||||
const zipName = attachmentsZipName({
|
||||
organizationName: notification?.bankClient?.shortName,
|
||||
notificationId: notification?.id ?? '',
|
||||
notificationCreationDate: notification?.createdAt ?? '',
|
||||
});
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
return createZip(notification?.attachments.map(el => el.attachmentId) ?? [], zipName).then(response =>
|
||||
downloadFile(response.data.attachmentId, response.data.attachmentToken)
|
||||
);
|
||||
},
|
||||
onSuccess: (response: any) => {
|
||||
showFile(response.data, response.fileName, response.type);
|
||||
queryClient.invalidateQueries([QUERY_KEY_USER_NOTIFICATION_BY_ID, id]);
|
||||
},
|
||||
onError: () => {
|
||||
showSnackbarMessage({ type: 'error', message: LOCALIZATION.ERROR_LOAD_ARCHIVE });
|
||||
},
|
||||
});
|
||||
if (isSignable) {
|
||||
openNotificationSignModal(notification, () => {
|
||||
readNotification(notification);
|
||||
});
|
||||
} else {
|
||||
readNotification(notification);
|
||||
}
|
||||
}, [isSignable, notification, readNotification]);
|
||||
|
||||
const handleClose = () => {
|
||||
goToNotificationsList();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (notification) {
|
||||
if (notification && !notification.isBlocked && isSuccess && !notification?.isRequiredRead) {
|
||||
readNotification(notification);
|
||||
}
|
||||
}, [notification, readNotification]);
|
||||
}, [isSuccess, notification, readNotification]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (isLoading && id) {
|
||||
@@ -353,21 +202,72 @@ const NotificationPageDrawer = ({ setBadge, readNotification, setNotificationExe
|
||||
execute();
|
||||
}, [execute]);
|
||||
|
||||
const actions: ModalButtonProps[] | undefined = useMemo(() => {
|
||||
if (isExecuteButtonVisible) {
|
||||
return [
|
||||
{
|
||||
text: notification.type.actionName || LOCALIZATION.EXECUTE,
|
||||
dataAction: 'execute',
|
||||
preventClose: true,
|
||||
isLoading: isExecuteLoading,
|
||||
onClick: handleClick,
|
||||
width: 'auto',
|
||||
variant: 'primary',
|
||||
},
|
||||
];
|
||||
const isFooterShown = useMemo(
|
||||
() =>
|
||||
[
|
||||
notification?.isRequiredRead && notification.status === READ_STATUS.READ && !isSigned,
|
||||
isSigned,
|
||||
isExecuteButtonVisible,
|
||||
isReadButtonVisible,
|
||||
].some(option => option),
|
||||
[notification, isSigned, isExecuteButtonVisible, isReadButtonVisible]
|
||||
);
|
||||
|
||||
const actions: ModalButtonProps[] = useMemo(() => {
|
||||
const secondaryButtons: ModalButtonProps[] = [];
|
||||
const primaryButtons: ModalButtonProps[] = [];
|
||||
|
||||
if (!isFooterShown || !notification) {
|
||||
return [];
|
||||
}
|
||||
}, [notification, handleClick, isExecuteLoading, isExecuteButtonVisible]);
|
||||
|
||||
if (isReadButtonVisible) {
|
||||
primaryButtons.push({
|
||||
text: LOCALIZATION.MARK_AS_READ,
|
||||
dataAction: 'read',
|
||||
preventClose: true,
|
||||
onClick: handleMarkAsRead,
|
||||
width: 'auto',
|
||||
variant: 'primary',
|
||||
});
|
||||
}
|
||||
|
||||
if (isExecuteButtonVisible) {
|
||||
primaryButtons.push({
|
||||
text: notification.type.actionName || LOCALIZATION.EXECUTE,
|
||||
dataAction: 'execute',
|
||||
preventClose: true,
|
||||
isLoading: isExecuteLoading,
|
||||
onClick: handleClick,
|
||||
width: 'auto',
|
||||
variant: 'primary',
|
||||
});
|
||||
}
|
||||
|
||||
if (isSigned) {
|
||||
secondaryButtons.push({
|
||||
text: LOCALIZATION.SIGNED,
|
||||
dataAction: 'signed',
|
||||
preventClose: true,
|
||||
icon: DoneIcon,
|
||||
disabled: true,
|
||||
width: 'auto',
|
||||
variant: 'secondary',
|
||||
});
|
||||
} else if (notification.isRequiredRead && notification.status === READ_STATUS.READ && !isSigned) {
|
||||
secondaryButtons.push({
|
||||
text: LOCALIZATION.READ,
|
||||
dataAction: 'read',
|
||||
preventClose: true,
|
||||
icon: DoneIcon,
|
||||
disabled: true,
|
||||
width: 'auto',
|
||||
variant: 'secondary',
|
||||
});
|
||||
}
|
||||
|
||||
return [...secondaryButtons, ...primaryButtons];
|
||||
}, [isFooterShown, notification, isReadButtonVisible, isExecuteButtonVisible, isSigned, handleMarkAsRead, isExecuteLoading, handleClick]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable compat/compat */
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import { Tabs } from '@fractal-ui/composites';
|
||||
import { Button, ButtonIcon, Skeleton, Spinner } from '@fractal-ui/core';
|
||||
import { Badge } from '@fractal-ui/extended';
|
||||
import { MailIcon } from '@fractal-ui/library';
|
||||
import { queryClient, EXECUTE_STATUS, IMPORTANCE_VALUES, type NotificationDto } from '@msb/http';
|
||||
import { queryClient, EXECUTE_STATUS, IMPORTANCE_VALUES, type NotificationDto, READ_STATUS } from '@msb/http';
|
||||
import {
|
||||
DataIsFailedError,
|
||||
EXTERNAL_PATHS,
|
||||
@@ -21,7 +19,7 @@ import {
|
||||
} from '@msb/shared';
|
||||
import { generatePath, useHistory } from 'react-router-dom';
|
||||
import { TABS, TABS_LABELS } from '../constants';
|
||||
import { LOCALIZATION } from '../constants/localization';
|
||||
import { LOCALIZATION } from '@/entities/UserNotification/constants';
|
||||
import { Filters } from './Filters';
|
||||
import { NotificationItem, NotificatioMobileItem } from './NotificationItem';
|
||||
import { NotificationPageDrawer } from './NotificationPageDrawer';
|
||||
@@ -117,6 +115,16 @@ const UserNotificationsListPage = () => {
|
||||
|
||||
const setBadge = useCallback(
|
||||
(notification: NotificationDto, isDrawerMode?: boolean): ReactNode => {
|
||||
if (notification.isRequiredRead && notification.status === READ_STATUS.READ) {
|
||||
return (
|
||||
<div>
|
||||
<Badge size="S" type="success">
|
||||
{LOCALIZATION.TAKEN_INTO_ACCOUNT}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (notification.executionStatus === EXECUTE_STATUS.EXECUTED) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const LOCALIZATION = {
|
||||
START: 'Начать',
|
||||
DEAR_CUSTOMER: 'Уважаемый клиент, с\u00A0наступающим\u00A0новым\u00A0годом!',
|
||||
DEAR_CUSTOMER: 'Уважаемый клиент',
|
||||
WE_ADAPTED_ONLINE_BANKING:
|
||||
'Мы адаптировали интернет-банк под задачи малого и среднего бизнеса. Вы в числе первых можете оценить обновленный дизайн и использовать новые возможности в работе.',
|
||||
INVALID_DATA: 'Некорректные данные',
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ const accountsTableGlobalStyles = css`
|
||||
`;
|
||||
|
||||
const ImageNewBank = styled.img`
|
||||
width: 226px;
|
||||
height: 190px;
|
||||
width: 320px;
|
||||
height: 160px;
|
||||
`;
|
||||
|
||||
export { accountsTableGlobalStyles, ImageNewBank };
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { FEATURE_TOGGLE_NAMES, network } from '@msb/http';
|
||||
import { EXTERNAL_URL_REGEXP, useFeatureToggles, useYaMetrika, YM_GOALS } from '@msb/shared';
|
||||
import {
|
||||
EXTERNAL_URL_REGEXP,
|
||||
IS_WELCOME_MESSAGE_READ_KEY,
|
||||
BLOCKING_NOTIFICATION_MODAL_KEY,
|
||||
useFeatureToggles,
|
||||
useYaMetrika,
|
||||
YM_GOALS,
|
||||
useAppContext,
|
||||
} from '@msb/shared';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface UseMegaBannerReturn {
|
||||
@@ -49,6 +57,7 @@ const EBG_BANNER_GOALS = {
|
||||
const useMegaBanners = (): UseMegaBannerReturn => {
|
||||
const history = useHistory();
|
||||
const { handleReachGoal } = useYaMetrika();
|
||||
const { isRequiredNotificationsLoaded, isRequiredNotificationsFailed } = useAppContext();
|
||||
const isMegaBannerConfirmedRef = useRef(false);
|
||||
const isEBGConfirmedRef = useRef(false);
|
||||
const hasSentEBGShowMetric = useRef(false);
|
||||
@@ -85,54 +94,58 @@ const useMegaBanners = (): UseMegaBannerReturn => {
|
||||
// проверка, какие баннеры нужно показать
|
||||
useEffect(() => {
|
||||
const checkBanners = () => {
|
||||
// if (localStorage.getItem(IS_WELCOME_MESSAGE_READ_KEY)) {
|
||||
const now = Date.now();
|
||||
let showEBG = false;
|
||||
let showMegaBanner = false;
|
||||
|
||||
const ebgCooldownData = localStorage.getItem(EBG_BANNER_COOLDOWN_KEY);
|
||||
|
||||
if (ebgCooldownData) {
|
||||
const { expiryDate } = JSON.parse(ebgCooldownData);
|
||||
|
||||
if (now >= expiryDate) {
|
||||
showEBG = true;
|
||||
}
|
||||
} else {
|
||||
showEBG = true;
|
||||
if (localStorage.getItem(BLOCKING_NOTIFICATION_MODAL_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showEBG) {
|
||||
const megaBannerCooldownData = localStorage.getItem(MEGA_BANNER_COOLDOWN_KEY);
|
||||
if (localStorage.getItem(IS_WELCOME_MESSAGE_READ_KEY) && (isRequiredNotificationsLoaded || isRequiredNotificationsFailed)) {
|
||||
const now = Date.now();
|
||||
let showEBG = false;
|
||||
let showMegaBanner = false;
|
||||
|
||||
if (megaBannerCooldownData) {
|
||||
const { expiryDate } = JSON.parse(megaBannerCooldownData);
|
||||
const ebgCooldownData = localStorage.getItem(EBG_BANNER_COOLDOWN_KEY);
|
||||
|
||||
if (ebgCooldownData) {
|
||||
const { expiryDate } = JSON.parse(ebgCooldownData);
|
||||
|
||||
if (now >= expiryDate) {
|
||||
const displayCount = Number(localStorage.getItem(DISPLAY_COUNT_KEY) || '0');
|
||||
|
||||
setCanIncrementCounter(true);
|
||||
|
||||
showMegaBanner = displayCount >= MEGA_BANNER_THRESHOLD;
|
||||
showEBG = true;
|
||||
}
|
||||
} else {
|
||||
const displayCount = Number(localStorage.getItem(DISPLAY_COUNT_KEY) || '0');
|
||||
showEBG = true;
|
||||
}
|
||||
|
||||
if (displayCount >= MEGA_BANNER_THRESHOLD) {
|
||||
showMegaBanner = true;
|
||||
if (!showEBG) {
|
||||
const megaBannerCooldownData = localStorage.getItem(MEGA_BANNER_COOLDOWN_KEY);
|
||||
|
||||
if (megaBannerCooldownData) {
|
||||
const { expiryDate } = JSON.parse(megaBannerCooldownData);
|
||||
|
||||
if (now >= expiryDate) {
|
||||
const displayCount = Number(localStorage.getItem(DISPLAY_COUNT_KEY) || '0');
|
||||
|
||||
setCanIncrementCounter(true);
|
||||
|
||||
showMegaBanner = displayCount >= MEGA_BANNER_THRESHOLD;
|
||||
}
|
||||
} else {
|
||||
const displayCount = Number(localStorage.getItem(DISPLAY_COUNT_KEY) || '0');
|
||||
|
||||
if (displayCount >= MEGA_BANNER_THRESHOLD) {
|
||||
showMegaBanner = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsEBGOpen(showEBG);
|
||||
setIsMegaBannerOpen(showMegaBanner);
|
||||
// }
|
||||
setIsEBGOpen(showEBG);
|
||||
setIsMegaBannerOpen(showMegaBanner);
|
||||
}
|
||||
};
|
||||
|
||||
if (isMegaBannerEnabled) {
|
||||
checkBanners();
|
||||
}
|
||||
}, [isMegaBannerEnabled]);
|
||||
}, [isMegaBannerEnabled, isRequiredNotificationsFailed, isRequiredNotificationsLoaded]);
|
||||
|
||||
// запускает счетчик после закрытия EBG баннера
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user