diff --git a/resources/css/components/entityHistory.css b/resources/css/components/entityHistory.css new file mode 100644 index 0000000..59fe29d --- /dev/null +++ b/resources/css/components/entityHistory.css @@ -0,0 +1,121 @@ +@import url('./../variables.css'); + +/* #modal-window__background{ + position: fixed; + display: block; + width: 100%; + height: 100%; + background: #6a6a6ab5; + top: 0; + z-index: 99; +} */ + +.entity-hist-container{ + position: fixed; + width: 50%; + top: 10%; + left: 25%; + z-index: 100; + background: white; + border-radius: 10px; + box-shadow: 0px 0px 15px 1px #9b9999; + + &>.hist__header-block{ + display: flex; + align-items: center; + justify-content: space-between; + background: var(--color_graphite_main); + min-height: 7%; + height: auto; + border-radius: 10px; + color: white; + padding: 10px; + + &>.hist__header__buttons{ + + &>.header__buttons__close{ + transform: rotate(45deg); + font-size: 2rem; + color: var(--color_ruby_main); + cursor: pointer; + } + } + } + + &>.hist__content-block{ + overflow-y: scroll; + padding: 10px; + max-height: 500px; + + &>.hist-container__date-block{ + display: flex; + + &>.date-block__date{ + flex-basis: 15%; + + &>div{ + position: sticky; + top: 0px; + padding: 3px; + background: var(--color_purple_main); + border-radius: 5px 0 0 5px; + color: white; + } + } + + &>.date-block__changes{ + flex-basis: 85%; + border-left: 2px solid black; + padding: 0 10px; + + &>.changes__action-block{ + + & .action-block__action-name{ + padding: 3px; + border-radius: 3px; + font-size: 1.2rem; + /* border-bottom: 3px solid var(--color_purple_main); */ + background: color-mix(in srgb, var(--color_purple_main) 20%, transparent); + box-shadow: 0px 2px 3px 0px #c0bdd3; + } + + &:last-child{ + margin-bottom: 30px; + } + + &>.changes__date-time-block{ + display: flex; + justify-content: space-between; + margin: 5px 0px 10px 0px; + color: rgb(173 173 173); + } + + &>.changes__details{ + margin-bottom: 15px; + + & .changes__details__el{ + display: flex; + justify-content: space-between; + margin-bottom: 10px; + + &>.changes__details__el__prop-name{ + flex-basis: 30%; + font-weight: 500; + } + + &>.changes__details__el__prop-val{ + flex-basis: 70%; + background: #91919121; + border-radius: 5px; + padding: 5px; + } + } + } + } + } + } + } + +} + + diff --git a/resources/js/components/entityHistory/EntityHistory.tsx b/resources/js/components/entityHistory/EntityHistory.tsx new file mode 100644 index 0000000..fa4183b --- /dev/null +++ b/resources/js/components/entityHistory/EntityHistory.tsx @@ -0,0 +1,120 @@ + +import React, { useEffect, useMemo, useState, useContext } from "react"; +import { createPortal } from "react-dom"; +import './../../../css/components/entityHistory.css'; +import { HistoryContext } from "../../contexts/HistoryContext"; + +//Тип с параметрами сущности, история по которой будет выводиться +export type EntityHistoryProps = { + changeAction: string, //Совершенное действие: создание, удаление, редактирование, архивирование и т.д. + changeAuthor: string, //Автор изменения: логин, сервисная УЗ + changeDate: Date //Объект даты изменения. В объекте будет либо время (тогда показываем), либо время будет отсутствовать (тогда не показываем) //что будем передавать? объект new Date или библиотечный объект для работы с датами? + changeTime?: string //Время изменений. Передавать не нужно, на этабе сборки компонента будет формироваться на основе пропса changeDate + // changeDetails?: HistoryEntityElDetails[] //Массив объектов с деталями изменений формата: изменяемое свойство(поле, например) - значение изменяемого свойства + changeDetails?: Record //Массив объектов с деталями изменений формата: изменяемое свойство(поле, например) - значение изменяемого свойства +}; + +//Тип с деталями изменений +// type HistoryEntityElDetails = { +// propName: string, //Имя измененного свойства +// propValue: string //Значение измененного свойства +// } + +//Компонент отображения истории изменений сущности +export default function EntityHistory () +{ + //ГАВРИЛОВ + //ИДЕНТИЧНЫЙ СПРАВОЧНИК УКАЗАН НА СТОРОНЕ ЛАРАВЕЛЬ APP/ENUMS/LOGBUSINESSEVENTS. ЕСТЬ ВОЗМОЖНОСТЬ ОБЪЕДИНИТЬ ИХ? + const HistoryActions = { + create: 'создание', + edit: 'редактирование', + archive: 'архивация', + restore: 'восстановление', + delete: 'удаление', + cancel: 'отмена', + } + const historyContext = useContext(HistoryContext); + //Перестраиваем объект с изменениями, группируя их по датам + const groupDataByDate = useMemo( () => { + let changesByDate = {}, + historyContent = historyContext.content.length ? historyContext.content : [] + //Сортируем все изменения от большего к меньшему по дате и времени + //ГАВРИЛОВ. или нужно отсортировать от меньшего к большему? мы хотим сверху самые актуальные данные а ниже менее актуальные или наоборот? + //ПРОВЕРИТЬ СОРТИРУЕТСЯ ЛИ ПО ДАТАМ + historyContent.sort( (a, b) => {return b.changeDate - a.changeDate} ) + //console.log(historyContext) + historyContent.forEach( (changeEl: EntityHistoryProps) => { + let dateToString = changeEl.changeDate.toLocaleString('ru-RU').split(',')[0]; + if ( !(dateToString in changesByDate) ) { + changesByDate[dateToString] = []; + } + changesByDate[dateToString].push(changeEl); + }); + + return changesByDate; + }, [historyContext.content]); + + + //Сокрытие блока показа истории + function hideHistoryBlock(){ + historyContext.hideHistory() + } + + return ( + historyContext.content && historyContext.visible ? + createPortal( +
+
+
+
+

{ historyContext.headerText.length ? historyContext.headerText : `История изменений запроса ${historyContext.entityId}` }

+
+
+ +
+
+
+ {Object.entries(groupDataByDate).map( (dateChanges, dateIndex) => +
+
+
{ dateChanges[0] }
+
+
+ { dateChanges[1].map( (changeEl: EntityHistoryProps, elIndex: number) => +
+
+ { HistoryActions[changeEl.changeAction] } +
+
+
+ { changeEl.changeAuthor } +
+
+ { changeEl.changeTime } +
+
+
+ {Object.entries(changeEl.changeDetails).map( (detail, detailIndex) => +
+
+ { (historyContext.dataFields && historyContext.dataFields[detail[0]]) ? historyContext.dataFields[detail[0]] : detail[0] } +
+
+ { detail[1] } +
+
+ )} +
+
+ )} +
+
+ )} +
+
+
, + document.body + ) + : '' + ) +} diff --git a/resources/js/contexts/HistoryContext.tsx b/resources/js/contexts/HistoryContext.tsx new file mode 100644 index 0000000..b076f71 --- /dev/null +++ b/resources/js/contexts/HistoryContext.tsx @@ -0,0 +1,97 @@ +import React, { createContext, useState, useContext } from "react"; +import EntityHistory, { EntityHistoryProps } from "../components/entityHistory/EntityHistory"; +import api from "../api"; +import { PreloaderContext } from "./PreloaderContext"; + +//Тип с пропсами для взаимодействия с контекстом +interface HistoryContextType { + visible: boolean, //Пропс видимости компонента + historyDataFields: HistoryDataFields, //Объект с названиями полей, по которым было совершено изменение. В БД они будут храниться, скорее всего, на латинице, на фронте их надо расшифровать + hideHistory: () => void, //Колбек для сокрытия блока с историей + content: Record | null, //Контент для отображения истории + headerText: string, //Текст в заголовке окна с историей + entityId: number, //Уникальный идентификатор сущности, по которой показываем историю (заявки, запрос и т.д.) + showHistory: (entityId: number, content: EntityHistoryProps[], windowHeader?: string) => void, //Колбек для вызова окна с историей + getHistoryFromMagic: (appname: string, subjectId: string | number, historyDataFields: HistoryDataFields, windowHeader?: string) => void //Метод получения истории из приложения Magic +} + +type HistoryDataFields = Record; + +export const HistoryContext = createContext(null); + +export const HistoryProvider = ( {children} ) => { + const [historyVisible, setHistoryVisible] = useState(false); + const [historyContent, setHistoryContent] = useState( [] ); + const [historyWindowHeader, setHistoryWindowHeader] = useState(''); + const [historyEntityId, setHistoryEntityId] = useState(); + const [historyDataFields, setHistoryDataFields] = useState( null ); + + /** + * Сокрытие окна с историей + */ + function hideHistoryBlock() { + setHistoryVisible(false); + setHistoryWindowHeader(''); + setHistoryContent( [] ); + setHistoryEntityId(0); + } + + const preloaderContext = useContext(PreloaderContext); + + //ГАВРИЛОВ. ПРАВИЛЬНО ЛИ В КОНТЕКСТЕ РЕАЛИЗОВЫВАТЬ ЛОГИКУ ФУНКЦИОНАЛА? ИЛИ ПРАВИЛЬНЕЕ ЕЕ РАЗМЕСТИТЬ В ФАЙЛЕ КОМПОНЕНТА? + async function getHistoryFromMagic(appName: string, subjectId: number | string, historyDataFields: HistoryDataFields, windowHeader?: string){ + preloaderContext.setPreloaderVisible(true) + const appHistory = await api.get(`${appName}/history/${subjectId}`).then(history => history.data.data); + + // console.log(appHistory) + + if (appHistory) { + appHistory.forEach( changeData => { + changeData.changeDetails = JSON.parse(changeData.changeDetails) + }); + //appHistory.changeDetails = JSON.parse(appHistory.changeDetails) + } + //ГАВРИЛОВ + //ПРОВЕРКА НА НАЛИЧИЕ НУЖНЫХ СВОЙСТВ. ЧТОБЫ ОБЪЕКТ СООТВЕТСТВОВАЛ ТИПУ + //УБРАТЬ CHANGE. ПРОСТО AUTHOR,DETAILS и так далее + + // console.log(appHistory) + + showHistoryBlock(subjectId, appHistory, historyDataFields, windowHeader) + + // return appHistory; + } + + /** + * Показываем историю + * @param entityId идентификатор бизнес-сущности + * @param content контент для отображения + * @param windowHeader заголовок для окна с историей + */ + //ГАВРИЛОВ. сделать пропс historyDataFields необязательным (пустой объект по умолчанию), так как details могут не передаваться, тогда и словарь не нужен (В ТОМ ЧИСЛЕ И В МЕТОДЕ getHistoryFromMagic). сделай интерфейс для аргументов идентичным + function showHistoryBlock(subjectId: number | string, content: EntityHistoryProps[], historyDataFields: HistoryDataFields, windowHeader?: string){ + preloaderContext.setPreloaderVisible(true) + setHistoryVisible(true); + setHistoryEntityId(subjectId); + setHistoryContent(content); + windowHeader ? setHistoryWindowHeader(windowHeader) : true; + historyDataFields ? setHistoryDataFields(historyDataFields) : true; + preloaderContext.setPreloaderVisible(false) + } + + const value = { + getHistoryFromMagic: getHistoryFromMagic, + showHistory: showHistoryBlock, + hideHistory: hideHistoryBlock, + visible: historyVisible, + content: historyContent, + headerText: historyWindowHeader, + entityId: historyEntityId, + dataFields: historyDataFields + }; + + return + {children} + + +}