добавляю все изменения проекта на текущий момент

This commit is contained in:
vasya
2026-02-27 18:49:27 +03:00
parent e927910fda
commit 9c35f4e35e
303 changed files with 79434 additions and 2558 deletions
+80
View File
@@ -0,0 +1,80 @@
import axios from 'axios';
/**
* @author dgavrilov
*/
//ГАВРИЛОВ
//переименуй файл на axios
//Инстанс api контура
const api = axios.create({
baseURL: '/public/api/',
withCredentials: true,
headers: {
'content-type': 'application/json',
'Accept': 'application/json',
}
})
//Инстанс web контура
const web = axios.create({
baseURL: '/public/',
withCredentials: true,
headers: {
'content-type': 'application/json',
'Accept': 'application/json',
}
})
//Гаврилов.
//Перехват запросов. Добавить подстановку bearer token?
// api.interceptors.request.use();
let isRefreshing = false, //Флаг выполнения запроса на обновления токена. Пока он иеет значение true, остальные запросы, попадающие в очередь из за полученной 401 ошибки кладутся в массив queueRqsts
queueRqsts = []; //запросы, которые получили 401 ошибку, одновремменно кладутся в эту переменную в виде промимсов, чтобы быть вызванными позднее, когда будет результат обновления токена превого запроса
//Если api ответ возвращает 401 ошибку, отправляем web запрос на роут фонового обновления санктум токена, где происходит проверка сессии, срока жизни санктум токена и его обновление, при удовлетворяющих условиях
//Перехват ответов api
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config; //Параметры вызываемого запроса
// originalRequest._retry = true;
if (error.response?.status === 401) {
if (isRefreshing) {
return new Promise( (resolve, reject) => {
queueRqsts.push({
resolve: () => resolve(api(originalRequest)),
reject
})
})
} else {
isRefreshing = true;
try {
let resultRefresh;
//ГАВРИЛОВ. запись данных формы в session storage при неудачном silent regresh token. Чтобы после перезагрузки страницы подтянуть данные из session storage в форму? тогда придется заморачиваться с компонентом подтягивания данных из session storage при рендеринге страницы
resultRefresh = await web.get('silent_token_refresh')
if (!resultRefresh.data.original.data.token_refresh) {
throw new Error('Сессия истекла');
}
queueRqsts.forEach(pending => {
pending.resolve();
})
return api(originalRequest);
} catch (error) {
queueRqsts.forEach(pending => pending.reject(error))
isRefreshing = false;
queueRqsts = [];
window.location.href = '/public/login';
} finally {
isRefreshing = false;
queueRqsts = [];
}
}
}
return true;
}
)
export default api;
+14
View File
@@ -0,0 +1,14 @@
import { createRoot } from 'react-dom/client';
import '@SharePoint/rencredit_uikit/dist/static/fonts/mont/Mont.css';
import '@fortawesome/fontawesome-free/css/all.css';
import { UIKitThemeProvider } from '@SharePoint/rencredit_uikit';
import MenuApp from './components/MenuApp.tsx'; // Создайте этот файл, если используете React
import React from 'react';
const container:HTMLElement = document.getElementById('root')!;
const root = createRoot(container);
root.render(
<UIKitThemeProvider>
<MenuApp />
</UIKitThemeProvider>
);
+30
View File
@@ -1,4 +1,34 @@
import 'bootstrap';
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });
+34
View File
@@ -0,0 +1,34 @@
import React from "react";
import { useEffect, useState } from "react";
import { getCsrfToken } from './../services/getCsrfService';
function MagicLogin ()
{
//Состояние с введенным логином
const [userLogin, setUserLogin] = useState<string>('');
return (
<div id="login-container">
<div id="login-container__magic-name">Magic</div>
<div id="login-container__form">
<form action="login" method="POST" autoComplete="on">
<input
type="hidden"
name="_token"
value={getCsrfToken()}
/>
<input name="_auth_login" type="text" onChange={ (e) => setUserLogin(e.target.value) } placeholder="Логин" required/>
<input name="_auth_password" type="password" placeholder="Пароль" required/>
<button type="submit" onClick={ () => {
//При сабмите кладем логин в локалсторадж для того, чтобы при вызове api ендпоинтов передавать его в заголовках
//гаврилов
//ТОЧНО ЛИ ЭТО НУЖНО? МОЖЕТ, ЛУЧШЕ ВСЕ API ЕНДПОИНТЫ ПЕРЕПИСАТ ПОД ВЫЗОВ WEB РОУТОВ?
localStorage.setItem('magic_auth_login', userLogin);
} }>Вход</button>
</form>
</div>
</div>
);
}
export default MagicLogin;
+108
View File
@@ -0,0 +1,108 @@
import React, { useEffect, useState } from "react";
import './../../css/components/magicPopup.css';
import { Button } from '@SharePoint/rencredit_uikit';
/**
* Компонент всплывающего окна
*/
/**
* Варианты типов для всплывающих окон: info - стандартный вид, error - произошла ошибка, attention - обратить внимание, success - успешное событие
*/
type PopupType = 'info' | 'success' | 'error' | 'attention';
//Интерфейс для передаваемых пропсов объекта всплывающего окна
interface MagicPopupProps {
key: number,
id: number, //Уникальный идентификатор, по котором всплывающее окно будет в том числе размонтироваться
message: string, //Текст всплывающего окна
renderIndex: number, //Индекс рендеринга компонента. Так как может отрисовываться сразу несколько popup, чтобы корректно их отрисовывать с анимацией с задержкой по времени, нужно передавать индекс компонента в наборе. Передавать "вручную" не нужно, он сам рассчитывается в компоненте MagicPopupContainer
type: PopupType, //Тип всплывающего окна, определяющий его внешний вид
timeOut?: boolean, //Если передается true, для элемента будет запущен стандартный таймер исчезновения. ВАЖНО! для всплывающих окон типа error таймер не будет активироваться, даже если передается. Пользователь должен гарантированно ознакомиться с сообщением и сам его закрыть
onClose?: (id: number) => void //Колбэк для полного размонтирования компонента со страницы
}
/**
* Компонент всплывающих окон
* @param props
* @returns
*/
export default function MagicPopup (props: MagicPopupProps)
{
//Объект с классами для иконок font-awesome в зависимости от переданного типа всплывающего окна. При изменении, необходимо поменять названия классов в magicPopup.css!
const iconNameObj = {
info: 'fa-circle-info',
success: 'fa-circle-check',
error: 'fa-circle-xmark',
attention: 'fa-circle-exclamation'
}
//С помощью деструктуризации указываем значение типа окна по умолчанию
const {message, timeOut = true, onClose} = props;
//Задержка исчезновения popup
const popupDelay = 300;
//Итоговая длительность таймера перед закрытием конкретного popup, учитывая его положение в наборе передаваемых попапов
const hideTimeOut: number = 3000 + popupDelay;
//Стейт для размонтирования попапа
const [popupVisible, setPopupVisible] = useState<boolean>(true)
//Стейт для плавного сокрытия popup. Класс плавного сокрытия присваивается за пол секунды до полного исчезновения через popupVisible, чтобы сначала плавно сокрыть блок из интерфейса, а затем полностью удалить его из DOM
const [popupHideClass, setPopupHideClass] = useState<string>('popup-visible')
//Стейт плавного появления popup. Каждый следующий popup в передаваемом наборе должен появляться с небольшой задержкой
const [popupShowClass, setPopupFadeinClass] = useState<string>('');
//Стейт запуска анимированного таймера исчезновения popup
const [popupTimer, setPopupTimer] = useState<string>('');
//Функция сокрытия popup. Сначала присваиваем класс, который анимированно скрывает popup, а через небольшую задержку размонтируем компонент
function hidePopupBlock (): void
{
setPopupHideClass('hidePopup');
setTimeout ( () => {
setPopupVisible(false);
}, 300)
//Вызываем метод удаления экземпляра компонента из набора. Так как попапы отрисовываются на основе компонента PopupContainer с набором компонентов MagicPopup, то после их размонтирования, их необходимо и удалить из набора в PopupContainer. Без их удаления, при вызове новых попапов, весь набор с уже просмотренными попапами будет снова отрисовываться.
onClose && onClose(props.id)
};
//При рендеринге сразу же запускается таймер сокрытия popup, если аргумент timeOut = true и тип окна != error
useEffect( () => {
if (timeOut && props.type !== 'error') {
setTimeout( () => {
//Задержка для отображения таймера нужна, так как при вызове рендеринга нескольких окон, каждое последующее появляется с задержкой. Без задержки ниже, таймер будет запускаться для всех popup одновременно и когда появится последний popup, его таймер уже может закончиться
setPopupTimer('timerProgress')
}, popupDelay)
//Через небольшую задержку после сокрытия компонента срабатывает таймер на размонтирование компонента и удаления его из набора
setTimeout( () => {
hidePopupBlock()
}, hideTimeOut)
}
//Появление popup при рендеринге
setTimeout ( () => {
setPopupFadeinClass('show')
}, popupDelay)
}, [])
return (
popupVisible ?
// <div onClick = {hidePopupBlock}>Раз</div>
<div className = {`magic-popup-container ${popupHideClass} ${popupShowClass}`} >
<div className = 'popup__icon-block'>
<i className = {`fa-solid ${iconNameObj[props.type]}`}></i>
</div>
<div className = "popup__content-block">
<div className = 'popup__content__text-block'>
{message}
</div>
<div className = "popup__button-block">
<Button
type = "button"
className = "popup__button-block__button"
onClick = {hidePopupBlock}
text = "OK"
/>
</div>
<div className = "popup__timer-block">
<div className = {`popup__timer-block__timer ${popupTimer}`}></div>
</div>
</div>
</div>
: null
);
};
@@ -0,0 +1,45 @@
import React, { useContext, ComponentProps } from "react";
import MagicPopup from "./MagicPopup";
import './../../css/components/magicPopupContainer.css';
import { PopupContext } from "../contexts/PopupContext";
/**
* Компонент контейнера для всплывающих окон, который вызывается на странице и куда передается массив всплывающих окон
*/
//ГАВРИЛОВ. ЛОГИЧНЕЕ ЭКСПОРТИРОВАТЬ ТИП С ПРОПСАМИ КОМПОНЕНТА ИЗ САМОГО КОМПОНЕНТА MAGICPOPUP?
/**
* Дополнительный экспорт типа MagicPopup из компонента всплывающего окна для возможности его импорта, в свою очередь, из компонента, собирающего страницу, например TaxiPage TaxiPage
*/
export type MagicPopupType = ComponentProps<typeof MagicPopup>;
/**
* Метод рендеринга контейнара для всплывающих окон
* @param {array} magicPopupArr массив объектов типа MagicPopup с информацией о всплывающем окне: текст, таймер и т.д.
* @param {callable} delHiddenPopupFunc колбэк для закрытия конкретного всплывающего окна по его уникальному идентификатору
* @returns ReactNode
*/
export default function MagicPopupContainer ()
{
const popupArr = useContext(PopupContext);
return (
//Родительский контейнер для всех всплывающих окон
<div id = "popup-parent-container">
{/* { magicPopupArr.map( (propsObj: MagicPopupType, index: number) => ( */}
{ popupArr.popupArrTest.map( (propsObj: MagicPopupType, index: number) => (
<MagicPopup
//Подробнее описание пропсов компонента смотри в MagicPopup.tsx
id = {propsObj.id}
key = {propsObj.id}
message = {propsObj.message}
timeOut = {propsObj.timeOut}
type = {propsObj.type}
renderIndex = {index}
//onClose = {delHiddenPopupFunc}
onClose = {popupArr.delPopupTest}
/>
))
}
</div>
)
}
+231
View File
@@ -0,0 +1,231 @@
import { Button } from '@SharePoint/rencredit_uikit';
import React, { useEffect, useState } from "react";
function MenuApp ()
{
//Приложения и процессы Magic для отображения в меню
const [menuApps, setMenuApps] = useState<{
'proccesses': object,
'scripts': object
}>({
'proccesses': {},
'scripts': {}
});
//TODO
//ВНЕДРИ В КОНТРОЛЛЕРЕ ПРОВЕРКУ ДОСТУПОВ, А НЕ ВОЗВРАЩАЙ ВСЕ ПОДРЯД ПРИЛОЖЕНИЯ
//Избранные приложения пользователя
const [favApps, setFavApps] = useState<string[]>([]);
//Массив избраннных процессов, в приложениях которых есть хотя бы одно избранное
const [favProcs, setFavProcs] = useState<string[]>([]);
//Состояния видимости скрипта
const [hideScriptClass, setHideScriptClass] = useState('');
//const [sanctumToken, setSanctumToken] = useState('');
const sanctumToken = () => {
const metaTag = document.getElementById('sanctum_token_block') as HTMLMetaElement;
return metaTag.dataset.token;
}
//Получаем при рендере страницы все приложения Magic и все избранные приложения пользователя
useEffect( () => {
Promise.all(
[
fetch('api/magic_apps').then(menuAppsRes => menuAppsRes.json()),
//TODO
//СЮДА ПОТЯГИВАЙ ЛОГИН ПОЛЬЗОВАТЕЛЯ, А НЕ DGAVRILOV
fetch('api/user_fav_app/dgavrilov').then(favAppsRes => favAppsRes.json())
]
).then(
([
responseMenuApp,
responseFavApp
]) => {
setMenuApps(responseMenuApp);
setFavApps(responseFavApp);
}
)
}, []);
//Обновление видимости процесса (собрания приложений) в зависимости от того находится ли в избранном хотя бы одно приложение
useEffect( () => {
if (menuApps === null) { return }
//Массив с процессами, в которых есть хотя бы одно избранное приложение
const favProcsArr:string[] = [];
//Мы собираем все избранные приложения в массив-состояние, чтобы при изменении состояния каждого приложения (например при переключении switcher) проверять входит ли это приложение в избранные. Если входит - его родительский процесс не прячем, так как в его дочерних приложениях есть избранные
Object.entries(menuApps.proccesses).forEach( (procData) => {
//Флаг - есть ли в приложениях процесса хотя бы 1 избранное
const hasFavorite: boolean = procData[1].tabs.split(';').some( (tab: string) => {
return favApps.includes(tab);
})
if (hideScriptClass !== 'script-hide' || hasFavorite) {
favProcsArr.push(procData[0]);
}
})
setFavProcs(favProcsArr);
}, [hideScriptClass, menuApps, favApps]);
//console.log(sanctumToken())
//ГАВРИЛОВ
//ПОЧЕМУ КОД НИЖЕ ИСПОЛНЯЕТСЯ ПОСТОЯННО ПОКА НЕ БУДЕТ ПОЛУЧЕН КОНТЕНТ ДЛЯ СТРАНИЦЫ ИЗ ПРОМИСОВ ВЫШЕ, А НЕ ОТРАБАТЫВАЕТ ТОЛЬКО ПРИ РЕНДЕРИНГЕ СТРАНИЦЫ
//Уместно ли отслеживать состояние до получения запросов fetch в подобном виде?
if (menuApps === null || favApps === null) {
return <span>прелоадер</span>
} else {
return (
<div id = 'menu-container'>
<div id = 'menu__left-block'>
<div className = 'menu__left-block__call-app'>
<i className = 'fa fa-th-large'></i>
<div className = 'mleft-block__call-app__title'>Приложения</div>
</div>
</div>
<div id = 'menu__app-container'>
<Button text='Текст' />
<div id = 'menu__app-container__app-block'>
{/* {console.log(menuApps.scripts)} */}
{Object.entries(menuApps.proccesses).map( (proc_val, proc_index) => (
<div className = "apps-block__proc" key = { proc_index } data-proc = {proc_val[0]}>
{/* В зависимости от того входит процесс в массив избранных делаем его видимым или скрываем */}
<div className = {`proc__title ${favProcs.includes(proc_val[0]) ? 'proc-visible' : 'proc-hide'}`}>{proc_val[1].title}</div>
<div className = "proc__script-list">
{proc_val[1].tabs.split(';').map( (app_el, app_index) => (
<AppElem
appIndex = { app_index }
appName = { app_el }
appTitle = { menuApps.scripts[app_el]?.title || "Неизвестный скрипт - " + app_el }
appUrl = { menuApps.scripts[app_el]?.url }
favIconClassName = { favApps.includes(app_el) ? 'favorite' : 'not_favorite' }
hideAppClass = { hideScriptClass }
//Функция обновления актуальный список избранных приложений
setUpdateFavApps = { (newFavApp) => setFavApps(newFavApp) }
/>
))}
</div>
</div>
)) }
</div>
<Switcher
toggleAppsVisible = { setHideScriptClass }
switcherId = "switcher-menu"
/>
</div>
</div>
)
}
}
//Гаврилов
//ПЕРЕПИСАТЬ АРГУМЕНТЫ ПОД ОБЪЕКТ СО СВОЙСТВАМИ, ИСПОЛЬЗОВАТЬ ...obj
/**
*
* @param {int} appIndex ключ для экземлпяра компонента
* @param {string} appName уникальное имя скрипта
* @param {string} appTitle название скрипта в меню
* @param {string} favIconClassName класс для иконки избранного
* @param {string} hideAppClass класс для видимости приложения
* @param {string} setUpdateFavApps функция для обновления состояния избранны приложений (при снятия, установки признака избранности)
* @returns
*/
function AppElem({
appIndex,
appName,
appTitle,
appUrl,
favIconClassName,
hideAppClass,
setUpdateFavApps
} : {
appIndex: string,
appName: string,
appTitle: string,
appUrl: string,
favIconClassName: string,
hideAppClass: string,
setUpdateFavApps: string
}) {
const [appElemClass, changeFav] = useState(favIconClassName);
const callChangeFav = ( changeAppName: string ) => {
fetch('api/user_fav_app', {
method: 'post',
body: JSON.stringify({ appName: changeAppName }),
headers: {
'content-type': 'application/json'
}
}
)
.then( (response) => response.json() )
.then( (response) => {
//Если скрипт был избранным - удаляем из избранного и наоборот
appElemClass == 'favorite' ? changeFav('not_favorite') : changeFav('favorite');
//Возвращаем обновленный список избранных приложений пользователя
setUpdateFavApps(response?.fav_apps?.split( ';' ) || [])
} )
}
return (
//Если приложение в избранном - оно всегда должно быть "видимо", даже если переключатель в положении Скрыть неизбранные приложения
<div key = { appIndex } className = {`script-list__el ${appElemClass == 'favorite' ? '' : hideAppClass}`} data-scriptname = { appName }>
<div className = "script-list__el__script-name"><a target="_blank" href={ appUrl }>{ appTitle }</a></div>
{/* Анонимная функция нужна для создания функции смены состояния для каждого компонента в отдельности, в противном случае вызов функции в одном экземпляре компонента вызывает функцию изменения для каждого экземпляра компонента */}
<i className = {`${(appElemClass == 'favorite' ? 'fas' : 'far')} fa-star ${appElemClass}`} onClick = { () => callChangeFav(appName) }></i>
</div>
)
}
//TODO
//ВЫНЕСИ В ПАПКУ COMPONENTS/MAIN
/**
* Компонент переключателя В ДАННОМ СЛУЧАЕ видимости скриптов (видны только избранные или все)
* @param {string} switcherId идентификатор переключателя (для стилей css), так как в будущем, на странице можно будет размещать несколько переключателей
* @param {function} toggleAppsVisible функция обновления состояния hideScriptClass
* @returns
*/
function Switcher({ switcherId, toggleAppsVisible }) {
const [favSwitcherClass, setFavSwitcherClass] = useState('showAll');
//Переключение состояния переключателя после рендеринга страницы
useEffect( () => {
//Получаем из локал сторадж состояние переключателя
const savedState = localStorage.getItem('magicMenuFavSwitcher');
if (savedState) {
setFavSwitcherClass(savedState);
}
}, []);
//При изменении состояния Переключателя изменяется состояние всех приложени (переключается их видимость в зависимости от состояния переключателя - только избранные или все)
useEffect( () => {
//Синхронизируем зависимые компоненты
toggleAppsVisible(favSwitcherClass === 'showFav' ? 'script-hide' : '');
}, [favSwitcherClass] );
const callToggleFavSwitcher = () => {
let switcherState = (favSwitcherClass === 'showFav' ? 'showAll' : 'showFav');
//favSwitcherClass => favSwitcherClass = ... - функциональное обновление. Здесь оно не особо нужно, но чтобы не забыть
//Меняем класс switcher, а также меняем состояние видимости всех приложений (в зависимости от того избранные они или нет)
setFavSwitcherClass( favSwitcherClass => {
if (favSwitcherClass === 'showFav') {
toggleAppsVisible('script-hide')
} else {
toggleAppsVisible('')
}
localStorage.setItem('magicMenuFavSwitcher', switcherState)
return switcherState;
} )
}
return (
<div id = { switcherId } className = "switcher-container">
<label className = "switch">
<input type = "checkbox" className = {`switcher__favorite-app ${favSwitcherClass}`} onChange = { callToggleFavSwitcher }/>
<span className = "slider round"></span>
</label>
</div>
)
}
export default MenuApp;
@@ -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<string, string> //Массив объектов с деталями изменений формата: изменяемое свойство(поле, например) - значение изменяемого свойства
};
//Тип с деталями изменений
// 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(
<div>
<div className="entity-hist-container">
<div className="hist__header-block">
<div className="hist__header__title">
<h3>{ historyContext.headerText.length ? historyContext.headerText : `История изменений запроса ${historyContext.entityId}` }</h3>
</div>
<div className="hist__header__buttons" onClick={hideHistoryBlock}>
<i className="fa fa-plus header__buttons__close"></i>
</div>
</div>
<div className="hist__content-block">
{Object.entries(groupDataByDate).map( (dateChanges, dateIndex) =>
<div key={dateIndex} className="hist-container__date-block">
<div className="date-block__date">
<div>{ dateChanges[0] }</div>
</div>
<div className="date-block__changes">
{ dateChanges[1].map( (changeEl: EntityHistoryProps, elIndex: number) =>
<div className="changes__action-block" key={ elIndex }>
<div className="action-block__action-name">
{ HistoryActions[changeEl.changeAction] }
</div>
<div className="changes__date-time-block">
<div>
{ changeEl.changeAuthor }
</div>
<div>
{ changeEl.changeTime }
</div>
</div>
<div className="changes__details">
{Object.entries(changeEl.changeDetails).map( (detail, detailIndex) =>
<div className="changes__details__el" key={ detailIndex }>
<div className="changes__details__el__prop-name">
{ (historyContext.dataFields && historyContext.dataFields[detail[0]]) ? historyContext.dataFields[detail[0]] : detail[0] }
</div>
<div className="changes__details__el__prop-val">
{ detail[1] }
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>,
document.body
)
: ''
)
}
@@ -0,0 +1,30 @@
import React, { useEffect, useState } from "react";
/**
* Объект с данными для отображения в блоке с ошибками
*/
export interface FormValidErrObject
{
fieldName: string | null,
fieldErrors: string[] | []
}
export default function FormValidErr ( {visible, validErrorsObj}: {visible: boolean, validErrorsObj: FormValidErrObject[]} )
{
const [blockVisible, setBlockVisible] = useState<boolean>(false);
useEffect ( () => {
setBlockVisible(visible)
}, [visible]);
return (
<div id="form-valid-err-container" className={`form-valid-err--${blockVisible ? "visible" : 'hide'}`}>
<ul>
{ validErrorsObj.map( (validErrObj: FormValidErrObject, index: number) => (
<li key={index}>
<b>{ validErrObj.fieldName }</b>:
<span>{ validErrObj.fieldErrors }</span>
</li>
)) }
</ul>
</div>
)
}
+51
View File
@@ -0,0 +1,51 @@
import React, { useEffect, useState } from "react";
import { Button } from '@SharePoint/rencredit_uikit';
export default function Header(){
const[appName, setAppName] = useState<string>('');
useEffect( () => {
setAppName(document.getElementById('page__header-block')?.dataset.app_name || '');
}, [])
return (
<div className="header-block__header-container">
<div className="header-container__block">
<div>
<Button
type = 'button'
onClick = {() => document.location.href='/public/menu'}
text = 'ЛОГОТИП ТУТ'
ui = 'secondaryPurple'
/>
</div>
<div>
<Button
type = 'button'
onClick = {() => document.location.href='/public/menu'}
text = 'Меню'
ui = 'secondaryPurple'
/>
</div>
<div className="header-container__block__app-name">{appName}</div>
</div>
<div className="header-container__block">
<div>
<Button
type = 'button'
onClick = {() => document.location.href='/public/request_access'}
text = 'Заказать доступ'
ui = 'secondaryPurple'
/>
</div>
<div>
<Button
type = 'button'
onClick = {() => document.location.href='/public/logout'}
text = 'Выход'
ui = 'secondaryPurple'
/>
</div>
</div>
</div>
)
}
@@ -0,0 +1,39 @@
import "./../../../css/components/preloader.css";
import React from "react";
export default function Preloader ( props: {visible: boolean, text?: string | null}) {
const {text = 'загрузка'} = props;
let preloaderColorArray: string[] = ['preloader-ruby-circle', 'preloader-emerald-circle', 'preloader-graphite-circle'],
//Код ниже реализует возможность присваивать каждую перезагрузку разные классы с цветом полос
//Рандомное число, по которому получим первый класс
firstColorIndex: number = Math.floor(Math.random() * 3),
firstCircleClass: typeof preloaderColorArray[number] = preloaderColorArray[firstColorIndex],
secondCircleClass: typeof preloaderColorArray[number],
thirdCircleClass: typeof preloaderColorArray[number];
//Удалим уже используемый класс цвета из массива
preloaderColorArray.splice(firstColorIndex, 1);
//Оставшиеся 2 класса распределеяем в зависимости от проостой проверки четности/нечетности
if (Math.floor(Math.random() * 2) % 2 === 0) {
secondCircleClass = preloaderColorArray[0];
thirdCircleClass = preloaderColorArray[1];
} else {
secondCircleClass = preloaderColorArray[1];
thirdCircleClass = preloaderColorArray[0];
}
return (
props.visible ?
<div id='preloader_container'>
<div id='preloader'>
<div className={'circle ' + firstCircleClass} id='circle_one'></div>
<div className={'circle ' + secondCircleClass} id='circle_two'></div>
<div className={'circle ' + thirdCircleClass} id='circle_three'></div>
<div id='logo'>
<div className='logo_square' id='right-bottom'></div>
<div className='logo_square' id='left-top'></div>
</div>
<div id='preloader_text'>{ text }</div>
</div>
</div>
: null
)
}
+97
View File
@@ -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<string, string> | 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<string, string>;
export const HistoryContext = createContext<HistoryContextType | null>(null);
export const HistoryProvider = ( {children} ) => {
const [historyVisible, setHistoryVisible] = useState<boolean>(false);
const [historyContent, setHistoryContent] = useState<EntityHistoryProps[] | []>( [] );
const [historyWindowHeader, setHistoryWindowHeader] = useState<string>('');
const [historyEntityId, setHistoryEntityId] = useState<number>();
const [historyDataFields, setHistoryDataFields] = useState<HistoryDataFields | null>( 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 <HistoryContext.Provider value={value}>
{children}
<EntityHistory/>
</HistoryContext.Provider>
}
+41
View File
@@ -0,0 +1,41 @@
import React, { createContext, useState } from "react";
import { MagicPopupType } from "../components/MagicPopupContainer";
import MagicPopupContainer from "../components/MagicPopupContainer";
//гаврилов. передай значением по умолчанию null и спроси у ChatGPT (deepseek не справляется) почему при указании null возникает ошибка в аргументе value
export const PopupContext = createContext('');
/**
* Контекст для компонента всплывающих окон
*/
export const PopupProvider = ({ children }) => {
const [popupArrTestVar, setPopupArrTestVar] = useState<MagicPopupType[] | []>( [] );
function addPopupTest (newPopupArrData: MagicPopupType[]) {
setPopupArrTestVar(prev => {
//Конкатенируем предыдущее состояние набора попапов и новые попапы, присваивая новому попапу параметр id с уникальным рандомным значением
return [...prev, ...newPopupArrData].map(popup => popup.id ? popup : {...popup, id: getRandomId()})
});
};
//Колбэк для удаления попапа из набора
function delPopupTest (popupDelKey: number) {
setPopupArrTestVar(prev => {
return prev.filter(popup => popup.id !== popupDelKey);
});
};
function getRandomId(): number { return Date.now() - Math.random() };
const value = {
popupArrTest: popupArrTestVar,
//ГАВРИЛОВ. переименуй
addPopupArrTest: addPopupTest,
delPopupArrTest: delPopupTest,
};
return <PopupContext.Provider value={value}>
{children}
<MagicPopupContainer />
</PopupContext.Provider>
}
@@ -0,0 +1,40 @@
import React, { createContext, useState } from "react";
import Preloader from "../components/preloader/Preloader";
interface PreloaderProps{
setPreloaderVisible: (preloaderVisible: boolean) => void;
setPreloaderText: (preloaderText: string) => void;
}
export const PreloaderContext = createContext<PreloaderProps>({
setPreloaderVisible: () => {},
setPreloaderText: () => {},
});
export function PreloaderProvider({ children }){
const [visible, setVisible] = useState<boolean>(true);
const [text, setText] = useState<string>('Страница загружается');
function setPreloaderVisible(preloaderVisible: boolean){
setVisible(preloaderVisible);
}
function setPreloaderText(preloaderText: string){
setText(preloaderText);
}
let value = {
setPreloaderVisible: setPreloaderVisible,
setPreloaderText: setPreloaderText
}
return (
<PreloaderContext.Provider value={value}>
<Preloader
visible={visible}
text={text}
/>
{children}
</PreloaderContext.Provider>
)
}
@@ -0,0 +1,32 @@
import { useState } from 'react';
import { MagicPopupType } from '../../components/MagicPopupContainer';
export const MagicPopupHook = () => {
// //Стейт с набором попапов для отрисовки
// const [popupArr, setPopupArr] = useState<MagicPopupType[] | []>( [] );
// //ИСПОЛЬЗОВАНИЕ USECALLBACK? я не использую его для функций удаления и добавления новых попапов в этом хуке, что должно приводить к созданию новых экземпляров функций каждый вызов хука (удаление, добавления попапа). Функция добавления попапа является пропсом для TaxiForm, значит, содание нового экземпляра функции добавления попапа в хуке должно вызывать перерендер TaxiForm? Но я его не замечаю (нет мигания в дом дереве элемента где рендерится TaxiForm ни при создании, не при удалении попапа)
// //Колбэк для добавление нового попапа. Его нужно передавать в каждый компонент, где планируется вызывать попапы. В аргументы этого колбэка передаются объекты с информацией для каждого попапа (текст, таймер и т.д.)
// function addPopup (newPopupArrData: MagicPopupType[]) {
// setPopupArr(prev => {
// //Конкатенируем предыдущее состояние набора попапов и новые попапы, присваивая новому попапу параметр id с уникальным рандомным значением
// return [...prev, ...newPopupArrData].map(popup => popup.id ? popup : {...popup, id: getRandomId()})
// });
// };
// //Колбэк для удаления попапа из набора
// function delPopup (popupDelKey: number) {
// setPopupArr(prev => {
// return prev.filter(popup => popup.id !== popupDelKey);
// });
// };
// //Функция генерации случайного числа для формирования на его основе уникального идентификатора для каждого компонента попаппа
// function getRandomId(): number { return Date.now() - Math.random() };
// return {
// popupArr,
// addPopup,
// delPopup,
// }
}
@@ -0,0 +1,26 @@
import React, { useState } from "react";
import Preloader from "../../components/preloader/Preloader";
//Хук для формирования пропсов управления состояний прелоадера, экспортируемых в компоненты страницы, где планируется вызывать прелоадер
export const MagicPreloaderHook = () => {
//Стейт для видимости прелоадера
const [preloaderVisibleState, setPreloaderVisibleState] = useState<boolean>(true);
//Стейт для текста прелоадера
const [preloaderTextState, setPreloaderTextState] = useState<string>('загрузка');
//Сеттер для смены состояний прелоадера
function setPreloaderParams (visibleState: boolean, textState?: string)
{
setPreloaderVisibleState(visibleState)
textState ? setPreloaderTextState(textState) : true, []
};
return {
setPreloaderParams,
PreloaderComponent: ( {preloaderVisible = preloaderVisibleState, preloaderText = preloaderTextState} ) => (
<Preloader
visible={preloaderVisible}
text={preloaderText}
/>
)
}
}
+7
View File
@@ -0,0 +1,7 @@
import { createRoot } from 'react-dom/client';
import MagicLogin from './components/MagicLogin.jsx';
import React from 'react';
const container = document.getElementById('root')!;
const root = createRoot(container);
root.render(<MagicLogin />);
+15
View File
@@ -0,0 +1,15 @@
import React from "react";
import { createRoot } from 'react-dom/client';
import { UIKitThemeProvider } from '@SharePoint/rencredit_uikit';
import Header from "./components/header/Header.tsx";
const headerBlock:HTMLElement = document.getElementById('page__header-block')!;
const headerRoot = createRoot(headerBlock);
console.log('da')
headerRoot.render(
<UIKitThemeProvider>
<Header />
</UIKitThemeProvider>
);
+21
View File
@@ -0,0 +1,21 @@
import React, { ReactNode } from "react";
import { UIKitThemeProvider } from '@SharePoint/rencredit_uikit';
import { PopupProvider } from "../contexts/PopupContext.tsx";
import { PreloaderProvider } from "../contexts/PreloaderContext.tsx";
interface AppProviderProps{
children: ReactNode;
}
export function AppProvider({children}: AppProviderProps){
return (
<UIKitThemeProvider>
<PopupProvider>
<PreloaderProvider>
{children}
</PreloaderProvider>
</PopupProvider>
</UIKitThemeProvider>
)
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Сервис для полуения csrt токена для размещения в формах
* @date 24.07.2025
* @author dgavrilov
*/
export const getCsrfToken = ():string => {
const METATAG:HTMLElement|null = document.querySelector('meta[name="csrf-token"]');
if (!METATAG) {
return '';
}
const CSRFTOKEN:string|null = METATAG.getAttribute('content');
if (!CSRFTOKEN) {
return '';
}
return CSRFTOKEN;
}