Compare commits

..

7 Commits
main ... main

Author SHA1 Message Date
Mikhail Chechnev
a20de79a40 ЦИТК-979 - Установка атрибутов сущности (клиент) 2025-08-14 14:21:23 +03:00
Mikhail Chechnev
fa00940776 ЦИТК-979 - Установка атрибутов сущности (сервер) 2025-08-14 14:17:25 +03:00
Mikhail Chechnev
4115961742 ЦИТК-979 - Контроль типов данных при организации связи, дополнительные события диаграммы (нажатие на атрибут, нажатие на панель), подсветка сущности при нажатии на атрибут, подсветка некорректных приёмников связи, функции получения атрибутики состояния атрибута сущности 2025-08-14 14:13:50 +03:00
Mikhail Chechnev
f6b29d5339 WEBAPP: в utils.js функция конвертации JSON в XML вынесена в отдельную object2XML (ранее была в составе object2Base64XML) 2025-08-14 14:07:12 +03:00
Mikhail Chechnev
b3e7d7bf7b ЦИТК-979 - Косметика в списке запросов "QueriesList" (сделан более компактным) 2025-08-14 13:56:35 +03:00
Mikhail Chechnev
f9b2f1ae34 ЦИТК-979 - Компонент для настройки свойств запроса, настройка атрибутов сущностей (начало, клиент) 2025-08-08 19:16:30 +03:00
Mikhail Chechnev
836a3db5e2 ЦИТК-979 - Формирование списка атрибутов сущности (сервер) 2025-08-08 19:09:59 +03:00
37 changed files with 1067 additions and 7238 deletions

View File

@ -105,11 +105,14 @@ const getDisplaySize = () => {
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
//Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => {
const object2XML = (obj, builderOptions) => {
const builder = new XMLBuilder(builderOptions);
return btoa(unescape(encodeURIComponent(builder.build(obj))));
return builder.build(obj);
};
//Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => btoa(unescape(encodeURIComponent(object2XML(obj, builderOptions))));
//Конвертация XML в JSON
const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => {
return new Promise((resolve, reject) => {
@ -200,6 +203,7 @@ export {
hasValue,
getDisplaySize,
deepCopyObject,
object2XML,
object2Base64XML,
xml2JSON,
formatDateRF,

View File

@ -1,316 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Панель мониторинга: Корневая панель доски задач
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useCallback } from "react"; //Классы React
import { DragDropContext, Droppable } from "react-beautiful-dnd"; //Работа с drag&drop
import { Stack, Box, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
import { StatusCard } from "./components/status_card.js";
import { TaskDialog } from "./task_dialog.js"; //Компонент формы события
import { Filter } from "./filter.js"; //Компонент фильтров
import { useExtraData, useColorRules, useStatuses } from "./hooks/hooks.js"; //Вспомогательные хуки
import { useTasks } from "./hooks/tasks_hooks.js"; //Хук событий
import { useFilters } from "./hooks/filter_hooks.js"; //Вспомогательные хуки фильтра
import { NoteDialog } from "./components/note_dialog.js"; //Диалог примечания
import { SettingsDialog } from "./components/settings_dialog.js"; //Диалог дополнительных настроек
import { deepCopyObject } from "../../core/utils.js"; //Вспомогательные функции
import { COMMON_STYLES } from "./styles"; //Общие стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
//---------
//Константы
//---------
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
CONTAINER: { width: "100%", padding: 0 },
BOX_FILTER: { display: "flex", alignItems: "center" },
ICON_BUTTON_SETTINGS: { marginLeft: "auto" },
STACK_STATUSES: { maxWidth: "99vw", paddingBottom: "5px", overflowX: "auto", ...COMMON_STYLES.SCROLL },
BOX_STATUSES: { position: "fixed", left: "8px", top: `calc(${APP_BAR_HEIGHT} + ${FILTER_HEIGHT})` }
};
//-----------
//Тело модуля
//-----------
//Корневая панель доски задач
const ClntTaskBoard = () => {
//Состояние фильтров
const [filters, handleFiltersChange] = useFilters();
//Состояние текущего загруженного фильтра
const [filterTypeLoaded, setFilterTypeLoaded] = useState(filters.values.sType);
//Состояние вспомогательных диалогов
const [dialogsState, setDialogsState] = useState({
filterDialogIsOpen: filters.isSetByUser,
settingsDialogIsOpen: false,
noteDialog: { isOpen: false, callback: null },
taskDialogIsOpen: false
});
//Состояние сортировок
const [orders, setOrders] = useState([]);
//Состояние дополнительных данных
const [extraData, setExtraData, handleDocLinksLoad] = useExtraData(filters.values.sType);
//Состояние статусов событий
const [statuses, statusesState, setStatuses, setStatusesState] = useStatuses(filters.values.sType);
//Состояние пользовательских настроек заливки событий
const [colorRules, setColorRules] = useColorRules();
//Состояние событий
const [tasks, setTasks, onDragEnd] = useTasks(filters.values, orders);
//Состояние доступных маршрутов события
const [availableRoutes, setAvailableRoutes] = useState({ source: "", routes: [] });
//Состояние перетаскиваемого события
const [dragItem, setDragItem] = useState({ type: "", status: "" });
//При открытии/закрытии диалога фильтра
const handleFilterOpen = isOpen => {
setDialogsState(pv => ({ ...pv, filterDialogIsOpen: isOpen }));
};
//При открытии/закрытии диалога дополнительных настроек
const handleSettingsOpen = () => setDialogsState(pv => ({ ...pv, settingsDialogIsOpen: !pv.settingsDialogIsOpen }));
//При открытии/закрытии диалога примечания
const handleNoteOpen = (cb = null) => {
setDialogsState(pv => ({ ...pv, noteDialog: { isOpen: !dialogsState.noteDialog.isOpen, callback: cb ? v => cb(v) : null } }));
};
//При открытии/закрытии диалога события
const handleTaskDialogOpen = () => setDialogsState(pv => ({ ...pv, taskDialogIsOpen: !dialogsState.taskDialogIsOpen }));
//При необходимости обновить дополнительные данные
const handleExtraDataReload = useCallback(() => {
setExtraData(pv => ({ ...pv, reload: true }));
}, [setExtraData]);
//При необходимости обновить информацию о событиях
const handleTasksReload = useCallback(
(bAccountsReload = true) => {
setTasks(pv => ({ ...pv, reload: true, accountsReload: bAccountsReload }));
},
[setTasks]
);
//При необходимости обновить состояние статусов
const handleStatusesStateReload = useCallback(() => {
setStatusesState(pv => ({ ...pv, reload: true, sorted: false }));
}, [setStatusesState]);
//При изменении дополнительных настроек
const handleSettingsChange = (newSettings, statusesState) => {
setColorRules(pv => ({ ...pv, selectedColorRule: newSettings.selectedColorRule }));
setStatusesState({ ...statusesState, sorted: false });
};
//При изменении цвета карточки статуса
const handleSettingStatusColorChange = (changedStatus, newColor) => {
//Считываем массив статусов
let newStatuses = [...statuses];
//Изменяем цвет нужного статуса
newStatuses.find(status => status.ID === changedStatus.ID).color = newColor;
//Обновляем состояние
setStatuses([...newStatuses]);
};
//При изменении сортировки
const handleOrderChanged = columnName => {
//Копируем состояние сортировки
let newOrders = deepCopyObject(orders);
//Находим сортируемую колонку
const orderedColumn = newOrders.find(o => o.name == columnName);
//Определяем направление сортировки
const newDirection = orderedColumn?.direction == "ASC" ? "DESC" : orderedColumn?.direction == "DESC" ? null : "ASC";
//Если сортировка отключается - очищаем информацию о сортировке
if (newDirection == null && orderedColumn) newOrders.splice(newOrders.indexOf(orderedColumn), 1);
//Если сортировки не было - устанавливаем
if (newDirection != null && !orderedColumn) newOrders.push({ name: columnName, direction: newDirection });
//Если сортировка была и не отключается - изменяем
if (newDirection != null && orderedColumn) orderedColumn.direction = newDirection;
//Устанавливаем новую сортировку
setOrders(newOrders);
};
//При необходимости очистки доступных маршрутов события
const handleAvailableRoutesStateClear = () => {
setAvailableRoutes({ source: "", routes: [] });
};
//Обработка захвата перетаскиваемого объекта
const handleDragItemChange = (filtersType, statusCode) =>
setDragItem({
type: filtersType,
status: statusCode
});
//Обработка очистки перетаскиваемого объекта
const handleDragItemClear = () => {
setDragItem({ type: "", status: "" });
};
//Проверка доступности карточки события
const isCardAvailable = code => {
return availableRoutes.source === code || availableRoutes.routes.find(r => r.SDESTINATION === code) || !availableRoutes.source ? true : false;
};
//При изменении фильтра
useEffect(() => {
//Если изменился тип
if (filters.loaded && filters.values.sType) {
//Если тип события изменился
if (filterTypeLoaded !== filters.values.sType) {
//Обновляем информацию о дополнительных данных
handleExtraDataReload();
//Обновляем информацию о статусах
handleStatusesStateReload();
//Обновляем текущий загруженный тип события
setFilterTypeLoaded(filters.values.sType);
}
//Обновляем информацию о событиях
handleTasksReload();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters.loaded, filters.values]);
//При изменении сортировки
useEffect(() => {
//Если есть все данные для загрузки событий
if (filters.loaded && filters.values.sType) {
//Обновляем информацию о событиях без обновления контрагентов
handleTasksReload(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [orders]);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
{dialogsState.settingsDialogIsOpen ? (
<SettingsDialog
initial={{ colorRules: colorRules, statusesState: statusesState }}
onSettingsChange={handleSettingsChange}
onClose={handleSettingsOpen}
/>
) : null}
{dialogsState.taskDialogIsOpen ? (
<TaskDialog
taskType={dragItem.type}
taskStatus={dragItem.status}
onTasksReload={() => handleTasksReload(true)}
onClose={() => {
handleTaskDialogOpen();
handleDragItemClear();
}}
/>
) : null}
<Box sx={STYLES.BOX_FILTER}>
<Stack direction="row">
<Filter
isFilterDialogOpen={dialogsState.filterDialogIsOpen}
filter={filters.values}
docLinks={extraData.docLinks}
selectedDocLink={filters.values.sDocLink ? extraData.docLinks.find(d => d.NRN === filters.values.sDocLink) : null}
onFilterChange={handleFiltersChange}
onDocLinksLoad={handleDocLinksLoad}
onFilterOpen={() => handleFilterOpen(true)}
onFilterClose={() => handleFilterOpen(false)}
onTasksReload={handleTasksReload}
orders={orders}
onOrderChanged={handleOrderChanged}
/>
</Stack>
<IconButton title="Настройки" onClick={handleSettingsOpen} sx={STYLES.ICON_BUTTON_SETTINGS}>
<Icon>settings</Icon>
</IconButton>
</Box>
{dialogsState.noteDialog.isOpen ? (
<NoteDialog noteTypes={extraData.noteTypes} onCallback={note => dialogsState.noteDialog.callback(note)} onClose={handleNoteOpen} />
) : null}
{filters.loaded && filters.values.sType && extraData.dataLoaded && tasks.loaded ? (
<DragDropContext
onDragStart={path => {
//Поиск кода текущего статуса задачи
let sourceCode = statuses.find(status => status.ID == path.source.droppableId).SEVNSTAT_CODE;
//Устанавливаем доступные маршруты события
setAvailableRoutes({ source: sourceCode, routes: [...extraData.evRoutes.filter(route => route.SSOURCE === sourceCode)] });
}}
onDragEnd={path => {
//Если есть статус назначения
if (path.destination) {
//Определяем мнемокод статуса назначения
let destCode = statuses.find(status => status.ID == path.destination.droppableId).SEVNSTAT_CODE;
//Переносим событие
onDragEnd({ path: path, eventPoints: extraData.evPoints, openNoteDialog: handleNoteOpen, destCode: destCode });
}
//Очищаем информацию о доступных маршрутах события
handleAvailableRoutesStateClear();
}}
>
<Box sx={STYLES.BOX_STATUSES}>
<Droppable droppableId="Statuses" type="droppableTask">
{provided => (
<div ref={provided.innerRef}>
<Stack direction="row" spacing={2} sx={STYLES.STACK_STATUSES}>
{statusesState.sorted
? statuses.map((status, index) => (
<div key={index}>
<Droppable
isDropDisabled={!isCardAvailable(status.SEVNSTAT_CODE)}
droppableId={status.ID.toString()}
>
{provided => (
<div ref={provided.innerRef}>
<StatusCard
tasks={tasks}
status={status}
statusTitle={status[statusesState.attr] || status.SEVNSTAT_NAME}
colorRules={colorRules}
extraData={extraData}
filtersType={filters.values.sType}
isCardAvailable={isCardAvailable}
onTasksReload={handleTasksReload}
onDragItemChange={handleDragItemChange}
onTaskDialogOpen={handleTaskDialogOpen}
onNoteDialogOpen={handleNoteOpen}
onStatusColorChange={handleSettingStatusColorChange}
placeholder={provided.placeholder}
/>
</div>
)}
</Droppable>
</div>
))
: null}
</Stack>
{provided.placeholder}
</div>
)}
</Droppable>
</Box>
</DragDropContext>
) : null}
</Box>
);
};
//----------------
//Интерфейс модуля
//----------------
export { ClntTaskBoard };

View File

@ -1,174 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Кастомное поле ввода
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { FormControl, InputLabel, Input, InputAdornment, IconButton, Icon, FormHelperText, Select, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
HELPER_TEXT: { color: "red" },
SELECT_MENU: width => {
return { ...COMMON_STYLES.SCROLL, width: width ? width + 24 : null };
}
};
//---------------
//Тело компонента
//---------------
//Кастомное поле ввода
const CustomInputField = ({
elementCode,
elementValue,
labelText,
onChange,
required = false,
items = null,
emptyItem = null,
dictionary,
menuItemRender,
...other
}) => {
//Значение элемента
const [value, setValue] = useState(elementValue);
//Состояние элемента HTML (для оптимизации ширины MenuItems)
const [anchorEl, setAnchorEl] = useState();
//При открытии меню заливки событий
const handleMenuOpen = e => {
//Устанавливаем элемент меню
setAnchorEl(e.target);
};
//При получении нового значения из вне
useEffect(() => {
setValue(elementValue);
}, [elementValue]);
//Изменение значения элемента
const handleChange = e => {
setValue(e.target.value);
if (onChange) onChange(e.target.name, e.target.value);
};
//Выбор значения из словаря
const handleDictionaryClick = () => {
dictionary ? dictionary(res => (res ? handleChange({ target: { name: elementCode, value: res } }) : null)) : null;
};
//Генерация поля с выбором из словаря Парус
const renderInput = validationError => {
//Генерация содержимого
return (
<Input
error={validationError}
id={elementCode}
name={elementCode}
value={value}
endAdornment={
dictionary ? (
<InputAdornment position="end">
<IconButton aria-label={`${elementCode} select`} onClick={handleDictionaryClick} edge="end">
<Icon>list</Icon>
</IconButton>
</InputAdornment>
) : null
}
aria-describedby={`${elementCode}-helper-text`}
label={labelText}
onChange={handleChange}
{...other}
/>
);
};
//Генерация поля с выпадающим списком
const renderSelect = (items, anchorEl, handleMenuOpen, validationError) => {
//Формируем общий список элементов меню
const menuItems = emptyItem ? [emptyItem, ...items] : [...items];
//Генерация содержимого
return (
<Select
error={validationError}
id={elementCode}
name={elementCode}
//!!!Пересмотреть момент. При изменении типа происходит ререндер со старым значением учетного документа:
//1. Изменяется тип
//2. Очищается items (список учетных документов)
//3. Рисуется компонент со старым value и пустым items, из-за чего ошибка "You have provided an out-of-range value"
//4. Вызывается useEffect, меняется значение value на новое (пустое значение)
value={value}
aria-describedby={`${elementCode}-helper-text`}
label={labelText}
MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU(anchorEl?.offsetWidth) } } }}
onChange={handleChange}
onOpen={handleMenuOpen}
{...other}
>
{menuItems
? menuItems.map((item, index) => {
let customRender = null;
if (menuItemRender) customRender = menuItemRender({ item: item, key: item?.key ?? index }) || null;
return customRender ? (
customRender
) : (
<MenuItem key={item?.key ?? index} value={item.id}>
<Typography variant="inherit" noWrap title={item.caption} component="div">
{item.caption}
</Typography>
</MenuItem>
);
})
: null}
</Select>
);
};
//Признак ошибки валидации
const validationError = !value && required ? true : false;
//Генерация содержимого
return (
<FormControl fullWidth variant="standard">
<InputLabel htmlFor={elementCode}>{labelText}</InputLabel>
{items ? renderSelect(items, anchorEl, handleMenuOpen, validationError) : renderInput(validationError)}
{validationError ? (
<FormHelperText id={`${elementCode}-helper-text`} sx={STYLES.HELPER_TEXT}>
*Обязательное поле
</FormHelperText>
) : null}
</FormControl>
);
};
//Контроль свойств - Кастомное поле ввода
CustomInputField.propTypes = {
elementCode: PropTypes.string.isRequired,
elementValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
labelText: PropTypes.string.isRequired,
required: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.object),
emptyItem: PropTypes.object,
dictionary: PropTypes.func,
onChange: PropTypes.func,
menuItemRender: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { CustomInputField };

View File

@ -1,336 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог фильтра отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Dialog,
DialogTitle,
IconButton,
Icon,
DialogContent,
DialogActions,
Button,
Box,
Stack,
Checkbox,
FormControlLabel,
Radio,
RadioGroup
} from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field"; //Кастомное поле ввода
import { hasValue } from "../../../core/utils"; //Вспомогательные функции
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
SELECT: { width: "450px" }
};
//---------------
//Тело компонента
//---------------
//Диалог фильтра отбора
const FilterDialog = ({ initial, onFilterChange, onFilterClose, onDocLinksLoad }) => {
//Собственное состояние
const [filter, setFilter] = useState(initial.filter);
//Состояние текущих учётных документов
const [curDocLinks, setCurDocLinks] = useState({ loaded: true, docLinks: initial.docLinks });
//Вспомогательные функции открытия раздела
const { handleCatalogTreeOpen, handleEventTypesOpen, handleAgnlistOpen, handleInsDepartmentOpen, handleCostStaffGroupsOpen } = useDictionary();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При изменении типа события фильтра
const handleTypeChange = callBack =>
handleEventTypesOpen({
sCode: filter.sType,
callBack: res => {
callBack(res.outParameters.eventtypecode);
}
});
//При изменении каталога фильтра
const handleCrnChange = callBack =>
handleCatalogTreeOpen({
sUnitName: "ClientEvents",
sName: filter.sCrnName,
callBack: res => {
callBack(res.outParameters.out_NAME);
}
});
//При изменении исполнителя фильтра
const handleSendPersonChange = callBack =>
handleAgnlistOpen({
sMnemo: filter.sSendPerson,
callBack: res => {
callBack(res.outParameters.agnmnemo);
}
});
//При изменении подразделения фильтра
const handleSendDivisionChange = callBack =>
handleInsDepartmentOpen({
sCode: filter.sSendDivision,
callBack: res => {
callBack(res.outParameters.out_CODE);
}
});
//При изменении группы пользователей фильтра
const handleSendUsrGrpChange = callBack =>
handleCostStaffGroupsOpen({
sCode: filter.sSendUsrGrp,
callBack: res => {
callBack(res.outParameters.out_CODE);
}
});
//Считывание подкаталогов
const getSubCatalogs = useCallback(async () => {
//Считываем каталоги
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SUBCATALOGS_GET",
args: {
SCRN_NAME: filter.sCrnName,
NSUBCAT: filter.bSubcatalogs ? 1 : 0
}
});
//Возвращаем список каталогов
return data.SRESULT;
}, [executeStored, filter.sCrnName, filter.bSubcatalogs]);
//При закрытии диалога с изменением фильтра
const handleDialogOk = async () => {
//Если указано имя каталога, но не загружен список рег. номеров
if (filter.sCrnName && !filter.sCrnRnList) {
//Загружаем список рег. номеров каталогов
const crns = await getSubCatalogs();
//Устанавливаем новый фильтр
onFilterChange({ ...filter, ...(crns ? { sCrnRnList: crns } : {}) });
//Закрываем диалог фильтра
onFilterClose();
} else {
//Устанавливаем новый фильтр
onFilterChange(filter);
//Закрываем диалог фильтра
onFilterClose();
}
};
//При очистке фильтра
const handleFilterClear = () => {
setFilter({
sState: EVENT_STATES[1],
sType: "",
sCrnName: "",
sCrnRnList: "",
bSubcatalogs: false,
sSendPerson: "",
sSendDivision: "",
sSendUsrGrp: "",
sDocLink: ""
});
};
//При изменении значения элемента
const handleFilterItemChange = (item, value) => {
//Если это изменение типа
if (item === "sType") {
//Указываем тип с очисткой информации об учетных документах
setFilter(pv => ({ ...pv, [item]: value, sDocLink: "" }));
setCurDocLinks(pv => ({ ...pv, loaded: false, docLinks: [] }));
} else {
//Обновляем значение поля
setFilter(pv => ({ ...pv, [item]: value }));
}
};
//При очистке учётного документа
const handleDocLinkClear = () => setFilter(pv => ({ ...pv, sDocLink: "" }));
//Обработка изменений с каталогами
useEffect(() => {
//Если каталог не указан, но галка подкаталогов установлена - снимаем её
if (!filter.sCrnName && filter.bSubcatalogs) setFilter(pv => ({ ...pv, bSubcatalogs: false }));
//Если изменился каталог и остался список рег. номеров каталогов - очищаем его
if (filter.sCrnName !== initial.sCrnName && filter.sCrnRnList) setFilter(pv => ({ ...pv, sCrnRnList: "" }));
//Если каталог равен изначальному
if (filter.sCrnName === initial.sCrnName) {
//Если признак подкаталогов равен изначальному, но список рег. номеров каталогов не соответствует - загружаем изначальный
if (filter.bSubcatalogs === initial.bSubcatalogs && filter.sCrnRnList !== initial.sCrnRnList) {
setFilter(pv => ({ ...pv, sCrnRnList: initial.sCrnRnList }));
}
//Если признак подкаталогов не равен изначальному
if (filter.bSubcatalogs !== initial.bSubcatalogs) {
//Если не установлен - считываем первый из списка рег. номеров изначальных каталогов
if (!filter.bSubcatalogs) {
setFilter(pv => ({
...pv,
sCrnRnList: initial.sCrnRnList.split(";")[0]
}));
} else {
//Если установлен - очищаем список рег. номеров каталогов для последующей загрузки
setFilter(pv => ({ ...pv, sCrnRnList: "" }));
}
}
}
}, [filter.sCrnName, filter.sCrnRnList, filter.bSubcatalogs, initial.sCrnName, initial.sCrnRnList, initial.bSubcatalogs]);
//Генерация содержимого
return (
<div>
<Dialog open onClose={onFilterClose} fullWidth maxWidth="sm">
<DialogTitle>Фильтр отбора</DialogTitle>
<IconButton aria-label="close" onClick={onFilterClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={COMMON_STYLES.SCROLL}>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Состояние</legend>
<RadioGroup
row
aria-labelledby="sState-label"
id="sState"
name="sState"
value={filter.sState}
onChange={e => handleFilterItemChange(e.target.name, e.target.value)}
>
{Object.keys(EVENT_STATES).map(function (k) {
return <FormControlLabel key={k} value={EVENT_STATES[k]} control={<Radio />} label={EVENT_STATES[k]} />;
})}
</RadioGroup>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sType"
elementValue={filter.sType}
labelText="Тип"
dictionary={callBack => handleTypeChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sCrnName"
elementValue={filter.sCrnName}
labelText="Каталог"
dictionary={callBack => handleCrnChange(callBack)}
onChange={handleFilterItemChange}
/>
<FormControlLabel
control={
<Checkbox
id="bSubcatalogs"
name="bSubcatalogs"
checked={filter.bSubcatalogs}
disabled={filter.sCrnName ? false : true}
onChange={e => handleFilterItemChange(e.target.name, e.target.checked)}
/>
}
label="Включая подкаталоги"
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendPerson"
elementValue={filter.sSendPerson}
labelText="Исполнитель"
dictionary={callBack => handleSendPersonChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendDivision"
elementValue={filter.sSendDivision}
labelText="Подразделение"
dictionary={callBack => handleSendDivisionChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendUsrGrp"
elementValue={filter.sSendUsrGrp}
labelText="Группа пользователей"
dictionary={callBack => handleSendUsrGrpChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={COMMON_STYLES.STACK_DOCLINKS}>
<CustomInputField
elementCode="sDocLink"
elementValue={filter.sDocLink}
labelText="Учётный документ"
items={[...(curDocLinks.docLinks || [])].reduce((prev, cur) => [...prev, { id: cur.NRN, caption: cur.SDESCR }], [])}
disabled={!curDocLinks.docLinks.length ? true : false}
onChange={handleFilterItemChange}
sx={STYLES.SELECT}
/>
<IconButton title="Очистить" disabled={!filter.sDocLink} onClick={handleDocLinkClear}>
<Icon>clear</Icon>
</IconButton>
<IconButton
title="Обновить"
disabled={curDocLinks.loaded}
onClick={() => {
//Очищаем учетный документ
handleDocLinkClear();
//Загружаем учетные документы типа
onDocLinksLoad(filter.sType).then(dl => setCurDocLinks(pv => ({ ...pv, loaded: true, docLinks: dl })));
}}
>
<Icon>refresh</Icon>
</IconButton>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button disabled={!hasValue(filter.sType)} variant="text" onClick={handleDialogOk}>
ОК
</Button>
<Button variant="text" onClick={handleFilterClear}>
Очистить
</Button>
<Button variant="text" onClick={onFilterClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалог фильтра отбора
FilterDialog.propTypes = {
initial: PropTypes.object.isRequired,
onFilterChange: PropTypes.func.isRequired,
onFilterClose: PropTypes.func.isRequired,
onDocLinksLoad: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { FilterDialog };

View File

@ -1,99 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог примечания
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, IconButton, Icon, DialogContent, DialogActions, Button, TextField } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { COMMON_STYLES } from "../styles"; //Общие стили
//Стили
const STYLES = {
DIALOG_CONTENT: { paddingTop: 0, paddingBottom: 0 }
};
//---------------
//Тело компонента
//---------------
//Диалог примечания
const NoteDialog = ({ noteTypes, onCallback, onClose }) => {
//Собственное состояние
const [note, setNote] = useState({ noteTypeIndex: 0, text: "" });
//При изменении примечания
const handleNoteChange = value => setNote(pv => ({ ...pv, text: value }));
//При изменении заголовка примечания
const handleNoteHeaderChange = (name, value) => {
setNote(pv => ({ ...pv, noteTypeIndex: value }));
};
//При закрытии диалога с изменением примечания
const handleDialogOk = () => {
//Передаем информацию о примечание в callback
onCallback({ header: noteTypes[note.noteTypeIndex], text: note.text });
onClose();
};
//Генерация содержимого
return (
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Примечание</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<CustomInputField
elementCode="noteHeader"
elementValue={note.noteTypeIndex}
labelText="Заголовок примечания"
items={noteTypes.reduce((prev, cur) => [...prev, { id: prev.length, caption: cur }], [])}
onChange={handleNoteHeaderChange}
margin="dense"
/>
<TextField
id="note"
label="Описание"
variant="standard"
fullWidth
required
multiline
minRows={7}
maxRows={7}
value={note.text}
margin="normal"
inputProps={{ sx: COMMON_STYLES.SCROLL }}
onChange={e => handleNoteChange(e.target.value)}
/>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button disabled={!note.text} variant="text" onClick={handleDialogOk}>
ОК
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог примечания
NoteDialog.propTypes = {
noteTypes: PropTypes.array,
onCallback: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { NoteDialog };

View File

@ -1,131 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог дополнительных настроек
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Icon, Button, Box, Stack } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { sortAttrs, sortDest } from "../layouts.js"; //Допустимые значение поля и направления сортировки
import { hasValue } from "../../../core/utils.js"; //Проверка наличия значения
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
SELECT: { width: "100%" }
};
//---------------
//Тело компонента
//---------------
//Диалог дополнительных настроек
const SettingsDialog = ({ initial, onSettingsChange, onClose, ...other }) => {
//Состояние дополнительных настроек
const [colorRules, seColorRules] = useState(initial.colorRules);
//Состояние статусов
const [statusesState, setStatusesState] = useState(initial.statusesState);
//Изменение поля сортировки
const handleSortAttrChange = (item, value) => setStatusesState(pv => ({ ...pv, [item]: value }));
//Изменение направления сортировки
const handleSortDestChange = newDirection => setStatusesState(pv => ({ ...pv, direction: newDirection }));
//При изменении правила заливки событий
const handleColorRuleChange = (item, value) => {
//Определяем новое правило заливки
let newColorRule = colorRules.rules[value];
//Обновляем в основных настройках
seColorRules(pv => ({ ...pv, selectedColorRule: newColorRule ? newColorRule : {} }));
};
//Генерация содержимого
return (
<div {...other}>
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={COMMON_STYLES.SCROLL}>
<Box component="section" p={1}>
<CustomInputField
elementCode="clrRules"
elementValue={hasValue(colorRules.selectedColorRule.id) && colorRules.length !== 0 ? colorRules.selectedColorRule.id : -1}
labelText="Заливка событий"
items={colorRules.rules.reduce((prev, cur) => [...prev, { id: cur.id, caption: cur.SDP_NAME }], [])}
emptyItem={{ key: -1, id: -1, caption: "-" }}
onChange={handleColorRuleChange}
sx={STYLES.SELECT}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={COMMON_STYLES.STACK_DOCLINKS}>
<CustomInputField
elementCode="attr"
elementValue={statusesState.attr}
labelText="Поле сортировки"
items={sortAttrs.reduce((prev, cur) => [...prev, { id: cur.id, caption: cur.descr }], [])}
onChange={handleSortAttrChange}
sx={STYLES.SELECT}
/>
<IconButton
title={statusesState.direction === "asc" ? "По возрастанию" : "По убыванию"}
onClick={() => handleSortDestChange(sortDest[sortDest.indexOf(statusesState.direction) * -1])}
>
<Icon>{statusesState.direction === "asc" ? "arrow_upward" : "arrow_downward"}</Icon>
</IconButton>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button
variant="text"
onClick={() => {
onSettingsChange(colorRules, statusesState);
onClose();
}}
>
ОК
</Button>
<Button
variant="text"
onClick={() => {
seColorRules(pv => ({ ...pv, selectedColorRule: {} }));
setStatusesState(pv => ({ ...pv, attr: "SEVNSTAT_NAME", direction: "asc" }));
}}
>
Очистить
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалог дополнительных настроек
SettingsDialog.propTypes = {
initial: PropTypes.object.isRequired,
onSettingsChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { SettingsDialog };

View File

@ -1,182 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка статуса событий
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Card, CardHeader, CardContent, Button, IconButton, Icon, Typography, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskCard } from "./task_card.js"; //Компонент Карточка события
import { StatusCardSettings } from "./status_card_settings.js"; //Компонент Диалог настройки карточки событий
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { COLORS } from "../layouts.js"; //Цвета статусов
import { APP_BAR_HEIGHT } from "../../../components/p8p_app_workspace"; //Заголовок страницы
//---------
//Константы
//---------
//Нижний отступ заголовка
const TITLE_PADDING_BOTTOM = "16px";
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
STATUS_BLOCK: statusColor => {
return {
width: "350px",
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`,
backgroundColor: statusColor,
padding: "8px"
};
},
BLOCK_OPACITY: isAvailable => {
return isAvailable ? { opacity: 1 } : { opacity: 0.5 };
},
CARD_HEADER_TITLE: {
textAlign: "left",
textOverflow: "ellipsis",
overflow: "hidden",
display: "-webkit-box",
hyphens: "auto",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
maxWidth: "calc(300px)",
width: "-webkit-fill-available",
fontSize: "1.2rem",
cursor: "default"
},
CARD_HEADER: { padding: 0 },
CARD_CONTENT: {
padding: 0,
paddingRight: "5px",
paddingBottom: "5px !important",
overflowY: "auto",
maxHeight: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 85px)`,
...APP_STYLES.SCROLL
}
};
//---------------
//Тело компонента
//---------------
//Карточка статуса события
const StatusCard = ({
tasks,
status,
statusTitle,
colorRules,
extraData,
filtersType,
isCardAvailable,
onTasksReload,
onDragItemChange,
onTaskDialogOpen,
onNoteDialogOpen,
onStatusColorChange,
placeholder
}) => {
//Состояние диалога настройки
const [statusCardSettingsOpen, setStatusCardSettingsOpen] = useState(false);
//Открыть/закрыть диалог настройки
const handleStatusCardSettingsOpen = () => setStatusCardSettingsOpen(!statusCardSettingsOpen);
//При изменении цвета статуса
const handleStatusColorChange = newColor => {
onStatusColorChange(status, newColor);
};
//Генерация содержимого
return (
<div>
{statusCardSettingsOpen ? (
<StatusCardSettings
statusColor={status.color}
availableColors={COLORS.includes(status.color) ? COLORS : [status.color, ...COLORS]}
onClose={handleStatusCardSettingsOpen}
onColorChange={handleStatusColorChange}
/>
) : null}
<Card
className="statusId-card"
sx={{
...STYLES.STATUS_BLOCK(status.color),
...STYLES.BLOCK_OPACITY(isCardAvailable(status.SEVNSTAT_CODE))
}}
>
<CardHeader
action={
<IconButton aria-label="settings" onClick={handleStatusCardSettingsOpen}>
<Icon>more_vert</Icon>
</IconButton>
}
title={
<Typography sx={STYLES.CARD_HEADER_TITLE} title={statusTitle} variant="h5">
{statusTitle}
</Typography>
}
subheader={
<Button
onClick={() => {
onDragItemChange(filtersType, status.SEVNSTAT_CODE);
onTaskDialogOpen();
}}
>
+ Добавить
</Button>
}
sx={STYLES.CARD_HEADER}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Stack spacing={1}>
{tasks.rows
.filter(item => item.sStatus === status.SEVNSTAT_NAME)
.map((item, index) => (
<TaskCard
task={item}
index={index}
onTasksReload={onTasksReload}
key={item.id}
colorRule={colorRules.selectedColorRule}
pointSettings={extraData.evPoints.find(p => p.SEVPOINT === status.SEVNSTAT_CODE)}
onOpenNoteDialog={onNoteDialogOpen}
/>
))}
{placeholder}
</Stack>
</CardContent>
</Card>
</div>
);
};
//Контроль свойств - Карточка статуса события
StatusCard.propTypes = {
tasks: PropTypes.object.isRequired,
status: PropTypes.object.isRequired,
statusTitle: PropTypes.string.isRequired,
colorRules: PropTypes.object.isRequired,
extraData: PropTypes.object.isRequired,
filtersType: PropTypes.string.isRequired,
isCardAvailable: PropTypes.func.isRequired,
onTasksReload: PropTypes.func.isRequired,
onDragItemChange: PropTypes.func.isRequired,
onTaskDialogOpen: PropTypes.func.isRequired,
onNoteDialogOpen: PropTypes.func.isRequired,
onStatusColorChange: PropTypes.func.isRequired,
placeholder: PropTypes.object.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { StatusCard };

View File

@ -1,109 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог настройки карточки статуса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, IconButton, Icon, DialogContent, DialogActions, Button, Box, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
BCKG_COLOR: backgroundColor => ({ backgroundColor: backgroundColor })
};
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Генерация элемента меню
const menuItemRender = ({ item, key }) => {
//Генерация содержимого
return (
<MenuItem key={key} value={item.id} sx={STYLES.BCKG_COLOR(item.caption)}>
<Typography variant="inherit" noWrap title={item.caption} component="div">
{item.caption}
</Typography>
</MenuItem>
);
};
//---------------
//Тело компонента
//---------------
//Диалог настройки карточки статуса
const StatusCardSettings = ({ statusColor, availableColors, onClose, onColorChange }) => {
//Состояние индекса текущего цвета
const [colorIndex, setColorIndex] = useState(availableColors.indexOf(statusColor));
//При закрытии диалога с применением настройки статуса
const handleDialogOk = () => {
//Изменяем цвет статуса
onColorChange(availableColors[colorIndex]);
//Закрываем диалог
onClose();
};
//При изменении значения элемента
const handleSettingsItemChange = (item, value) => {
setColorIndex(value);
};
//Генерация содержимого
return (
<div>
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent>
<Box component="section" p={1}>
<CustomInputField
elementCode="color"
elementValue={colorIndex}
labelText="Цвет"
items={availableColors.reduce((prev, cur) => [...prev, { id: prev.length, caption: cur }], [])}
onChange={handleSettingsItemChange}
sx={STYLES.BCKG_COLOR(availableColors[colorIndex])}
menuItemRender={menuItemRender}
/>
</Box>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button variant="text" onClick={handleDialogOk}>
Применить
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств - Диалог настройки карточки статуса
StatusCardSettings.propTypes = {
statusColor: PropTypes.string.isRequired,
availableColors: PropTypes.arrayOf(PropTypes.string).isRequired,
onClose: PropTypes.func.isRequired,
onColorChange: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { StatusCardSettings };

View File

@ -1,376 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Draggable } from "react-beautiful-dnd"; //Работа с drag&drop
import { Card, CardHeader, Typography, IconButton, Icon, Box, Menu, MenuItem, CardContent, Avatar, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskDialog } from "../task_dialog"; //Форма события
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений
import { TASK_COLORS, getTaskExpiredColor, getTaskBgColorByRule, makeCardActionsArray } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
import { useTasksFunctions } from "../hooks/tasks_hooks"; //Состояние вспомогательных функций событий
//---------
//Константы
//---------
//Стили
const STYLES = {
MENU_ITEM_DELIMITER: { borderBottom: "1px solid lightgrey" },
CARD: (indicatorClr, bgClr) => {
const i = indicatorClr ? { borderLeft: `solid ${indicatorClr}` } : null;
const bc = bgClr ? { backgroundColor: bgClr } : null;
return { ...i, ...bc };
},
CARD_HEADER_TITLE: {
padding: "4px",
width: "292px",
display: "-webkit-box",
hyphens: "auto",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
overflow: "hidden"
},
CARD_HEADER: { padding: 0, cursor: "pointer" },
CARD_CONTENT: { padding: "4px !important" },
CARD_CONTENT_BOX: { display: "flex", alignItems: "center" },
STACK_SENDER: { alignItems: "center", marginLeft: "auto" },
TYPOGRAPHY_SECONDARY: {
color: "text.secondary",
fontSize: 14
},
ICON_COLOR: linked => {
return { color: theme => (linked ? TASK_COLORS.LINKED : theme.palette.grey[500]) };
}
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Действия карточки события
const CardActions = ({
taskRn,
menuItems,
cardActions,
onMethodsMenuButtonClick,
onMethodsMenuClose,
onTasksReload,
pointSettings,
onOpenNoteDialog
}) => {
//При нажатии на действие меню
const handleActionClick = action => {
//Выполняем действие
action.func({
nEvent: taskRn,
onReload: action.tasksReload ? () => onTasksReload(action.needAccountsReload) : null,
onNoteOpen: pointSettings.ADDNOTE_ONSEND ? onOpenNoteDialog : null
});
//Закрываем меню действий
onMethodsMenuClose();
};
return (
<Box sx={STYLES.BOX_ROW}>
<IconButton id={`${taskRn}_menu_button`} aria-haspopup="true" onClick={onMethodsMenuButtonClick}>
<Icon>more_vert</Icon>
</IconButton>
<Menu id={`${taskRn}_menu`} anchorEl={cardActions.anchorMenuMethods} open={cardActions.openMethods} onClose={onMethodsMenuClose}>
{menuItems.map(action => {
if (action.visible)
return (
<MenuItem
sx={action.delimiter ? STYLES.MENU_ITEM_DELIMITER : {}}
key={`${taskRn}_${action.method}`}
onClick={() => handleActionClick(action)}
>
<Icon>{action.icon}</Icon>
<Typography pl={1}>{action.name}</Typography>
</MenuItem>
);
})}
</Menu>
</Box>
);
};
//Контроль свойств - Действия карточки события
CardActions.propTypes = {
taskRn: PropTypes.number.isRequired,
menuItems: PropTypes.array.isRequired,
cardActions: PropTypes.object.isRequired,
onMethodsMenuButtonClick: PropTypes.func.isRequired,
onMethodsMenuClose: PropTypes.func.isRequired,
onTasksReload: PropTypes.func,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Карточка события
const TaskCard = ({ task, index, onTasksReload, colorRule, pointSettings, onOpenNoteDialog }) => {
//Состояние диалога события
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
//Состояние действий события
const [cardActions, setCardActions] = useState({ anchorMenuMethods: null, openMethods: false });
//Состояние списка действий меню
const [menuItems, setMenuItems] = useState([]);
//Вспомогательные функции открытия раздела
const { handleClientEventsOpen, handleClientEventsNotesOpen, handleFileLinksOpen } = useDictionary();
//Состояние вспомогательных функций событий
const { handleTaskStateChange, handleTaskSend } = useTasksFunctions();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Подключение к контексту приложения
const { pOnlineShowDocument } = useContext(ApplicationСtx);
//По нажатию на открытие меню действий
const handleMethodsMenuButtonClick = useCallback(event => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: event.currentTarget, openMethods: true }));
}, []);
//При закрытии меню
const handleMethodsMenuClose = useCallback(() => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: null, openMethods: false }));
}, []);
//При удалении контрагента
const handleTaskDelete = useCallback(
async ({ nEvent, onReload }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DELETE",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При возврате в предыдущую точку события
const handleTaskReturn = useCallback(
async ({ nEvent, onReload }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_RETURN",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//По нажатию действия "Направить"
const handleSendAction = useCallback(
async ({ nEvent, onReload, onNoteOpen }) => {
//Выполняем направление события
handleTaskSend({ nEvent, onReload, onNoteOpen });
},
[handleTaskSend]
);
//По нажатия действия "Редактировать"
const handleTaskEditAction = useCallback(() => {
setTaskDialogOpen(true);
}, []);
//По нажатия действия "Редактировать в разделе"
const handleTaskEditClientAction = useCallback(
async ({ nEvent }) => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SELECT",
args: {
NCLNEVENTS: nEvent
}
});
if (data.NIDENT) {
//Открываем раздел "События" с фильтром по записи
handleClientEventsOpen({ nIdent: data.NIDENT });
}
},
[executeStored, handleClientEventsOpen]
);
//По нажатию действия "Удалить"
const handleTaskDeleteAction = useCallback(
({ nEvent, onReload }) => {
showMsgWarn("Удалить событие?", () => handleTaskDelete({ nEvent, onReload }));
},
[handleTaskDelete, showMsgWarn]
);
//По нажатию действия "Выполнить возврат"
const handleTaskReturnAction = useCallback(
({ nEvent, onReload }) => {
showMsgWarn("Выполнить возврат события в предыдущую точку?", () => handleTaskReturn({ nEvent, onReload }));
},
[handleTaskReturn, showMsgWarn]
);
//По нажатию действия "Примечания"
const handleEventNotesOpenAction = useCallback(
({ nEvent }) => {
handleClientEventsNotesOpen({ nPrn: nEvent });
},
[handleClientEventsNotesOpen]
);
//По нажатию действия "Присоединенные документы"
const handleTaskFileLinksOpenAction = useCallback(
({ nEvent }) => {
handleFileLinksOpen({ nPrn: nEvent, sUnitCode: "ClientEvents" });
},
[handleFileLinksOpen]
);
//По нажатию действия "Перейти"
const handleStateChangeAction = useCallback(
async ({ nEvent, onReload, onNoteOpen }) => {
//Выполняем изменения статуса события
handleTaskStateChange({ nEvent, onReload, onNoteOpen });
},
[handleTaskStateChange]
);
//При изменении ссылок в меню действий (для того, чтобы ссылка на объект менялась при реальной необходимости)
useEffect(() => {
//Устанавливаем список меню
setMenuItems(
makeCardActionsArray(
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskDeleteAction,
handleStateChangeAction,
handleTaskReturnAction,
handleSendAction,
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction
)
);
}, [
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction,
handleSendAction,
handleStateChangeAction,
handleTaskDeleteAction,
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskReturnAction
]);
//Генерация содержимого
return (
<Box>
{taskDialogOpen ? (
<TaskDialog
taskRn={task.nRn}
taskType={task.sType}
editable={pointSettings.BAN_UPDATE ? false : true}
onTasksReload={onTasksReload}
onClose={() => {
setTaskDialogOpen(false);
}}
/>
) : null}
<Draggable draggableId={task.id.toString()} key={task.id} index={index}>
{provided => (
<Card
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
sx={STYLES.CARD(getTaskExpiredColor(task), colorRule.SCOLOR ? getTaskBgColorByRule(task, colorRule) : null)}
>
<CardHeader
title={
<Typography
className="task-info"
sx={STYLES.CARD_HEADER_TITLE}
lang="ru"
onClick={() => {
menuItems.find(action =>
action.method === "EDIT" ? action.func(task.nRn, action.tasksReload ? onTasksReload : null) : null
);
}}
>
{task.sDescription}
</Typography>
}
sx={STYLES.CARD_HEADER}
action={
<CardActions
taskRn={task.nRn}
menuItems={menuItems}
cardActions={cardActions}
onMethodsMenuButtonClick={handleMethodsMenuButtonClick}
onMethodsMenuClose={handleMethodsMenuClose}
onTasksReload={onTasksReload}
pointSettings={pointSettings}
onOpenNoteDialog={onOpenNoteDialog}
/>
}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Box sx={STYLES.CARD_CONTENT_BOX}>
<IconButton
title={task.nLinkedRn ? "Событие получено по статусной модели" : null}
onClick={
task.nLinkedRn ? () => pOnlineShowDocument({ unitCode: task.sLinkedUnit, document: task.nLinkedRn }) : null
}
sx={STYLES.ICON_COLOR(task.nLinkedRn)}
disabled={!task.nLinkedRn}
>
<Icon>assignment</Icon>
</IconButton>
<Typography sx={STYLES.TYPOGRAPHY_SECONDARY}>{task.name}</Typography>
{task.sSender ? (
<Stack direction="row" spacing={0.5} sx={STYLES.STACK_SENDER}>
<Typography sx={STYLES.TYPOGRAPHY_SECONDARY}>{task.sSender}</Typography>
<Avatar src={task.avatar ? `data:image/png;base64,${task.avatar}` : null} />
</Stack>
) : null}
</Box>
</CardContent>
</Card>
)}
</Draggable>
</Box>
);
};
//Контроль свойств - Карточка события
TaskCard.propTypes = {
task: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
onTasksReload: PropTypes.func,
colorRule: PropTypes.object,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { TaskCard };

View File

@ -1,158 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Форма события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Typography, Tabs, Tab, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
import { TaskFormTabInfo } from "./task_form_tab_info"; //Вкладка основной информации
import { TaskFormTabExecutor } from "./task_form_tab_executor"; //Вкладка информации об исполнителе
import { TaskFormTabProps } from "./task_form_tab_props"; //Вкладка информации со свойствами
import { useDocsProps } from "../hooks/task_dialog_hooks"; //Хук для получения свойств раздела "События"
import { hasValue } from "../../../core/utils";
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { margin: "5px 0px", textAlign: "center" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Свойства вкладки
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
//Вкладка информации
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
//Генерация содержимого
return (
<Box role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other}>
{value === index && <Box pt={1}>{children}</Box>}
</Box>
);
}
//Контроль свойств - Вкладка информации
CustomTabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
//Формирование кнопки для открытия раздела
export const getInputProps = (onClick, disabled = false, icon = "list") => {
//Генерация содержимого
return {
endAdornment: (
<InputAdornment position="end">
<IconButton disabled={disabled} aria-label={`select`} onClick={onClick} edge="end">
<Icon>{icon}</Icon>
</IconButton>
</InputAdornment>
)
};
};
//-----------
//Тело модуля
//-----------
//Форма события
const TaskForm = ({ task, taskType, onTaskChange, editable, onEventNextNumbGet, onDPReady }) => {
//Состояние вкладки
const [tab, setTab] = useState(0);
//Состояние допустимых дополнительных свойств
const [docProps] = useDocsProps(taskType);
//При изменении вкладки
const handleTabChange = (e, newValue) => {
setTab(newValue);
};
//При изменении поля
const handleFieldEdit = useCallback(
e => {
onTaskChange({
[e.target.id]: e.target.value,
//Связанные значения, если меняется одно, то необходимо обнулить другое
...(e.target.id === "sClntClnperson" ? { sClntClients: "" } : {}),
...(e.target.id === "sClntClients" ? { sClntClnperson: "" } : {})
});
},
[onTaskChange]
);
//При изменении доп. свойства
const handlePropEdit = useCallback(
(docProp, value) => {
onTaskChange({ docProps: { ...task.docProps, [docProp]: value } });
},
[onTaskChange, task.docProps]
);
//Проверка заполненности всех обязательных доп. свойств
useEffect(() => {
//Считываем количество незаполненных обязательных свойств
let notFilled = docProps.props.filter(docProp => docProp.BREQUIRE === true && !hasValue(task.docProps[docProp.SFORMATTED_ID])).length;
//Если доп. свойства загрузились и количество незаполненных = 0 - доп. свойства готовы, иначе не готовы
docProps.loaded && notFilled === 0 ? onDPReady(true) : onDPReady(false);
}, [docProps, onDPReady, task.docProps]);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
<Typography pb={1} variant="h6">
{task.nRn ? "Исправление события" : "Добавление события"}
</Typography>
<Tabs value={tab} onChange={handleTabChange} aria-label="tabs of values">
<Tab label="Событие" {...a11yProps(0)} />
<Tab label="Исполнитель" {...a11yProps(1)} />
{docProps.props.length > 0 ? <Tab label="Свойства" {...a11yProps(2)} /> : null}
</Tabs>
<CustomTabPanel value={tab} index={0}>
<TaskFormTabInfo task={task} editable={editable} onFieldEdit={handleFieldEdit} onEventNextNumbGet={onEventNextNumbGet} />
</CustomTabPanel>
<CustomTabPanel value={tab} index={1}>
<TaskFormTabExecutor task={task} onFieldEdit={handleFieldEdit} />
</CustomTabPanel>
{docProps.props.length > 0 ? (
<CustomTabPanel value={tab} index={2}>
<TaskFormTabProps task={task} taskType={taskType} docProps={docProps} onPropEdit={handlePropEdit} />
</CustomTabPanel>
) : null}
</Box>
);
};
//Контроль свойств - Форма события
TaskForm.propTypes = {
task: PropTypes.object.isRequired,
taskType: PropTypes.string.isRequired,
onTaskChange: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired,
onDPReady: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskForm };

View File

@ -1,155 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации об исполнителе
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_LEFT_ALIGN: { display: "flex", justifyContent: "flex-start" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации об исполнителе
const TaskFormTabExecutor = ({ task, onFieldEdit }) => {
//Вспомогательные функции открытия раздела
const { handleClientPersonOpen } = useDictionary();
//При изменении сотрудника-инициатора
const handleInitClnpersonChange = () =>
handleClientPersonOpen({
sCode: task.sInitClnperson,
callBack: res => {
onFieldEdit({
target: {
id: "sInitClnperson",
value: res.outParameters.out_CODE
}
});
}
});
//Генерация содержимого
return (
<Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_LEFT_ALIGN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Планирование</legend>
<TextField
id="dPlanDate"
label="Начало работ"
InputLabelProps={{ shrink: true }}
type="datetime-local"
variant="standard"
value={task.dPlanDate ? dayjs(task.dPlanDate, "DD.MM.YYYY HH:mm").format("YYYY-MM-DD HH:mm") : ""}
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Инициатор</legend>
<TextField
id="sInitClnperson"
label="Сотрудник"
value={task.sInitClnperson}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
InputProps={getInputProps(() => handleInitClnpersonChange(), task.isUpdate)}
></TextField>
<TextField id="sInitUser" label="Пользователь" value={task.sInitUser} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sInitReason"
label="Основание"
value={task.sInitReason}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Направить</legend>
<TextField id="sToCompany" label="Организация" value={task.sToCompany} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToDepartment"
label="Подразделение"
value={task.sToDepartment}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sToClnpost" label="Должность" value={task.sToClnpost} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToClnpsdep"
label="Штатная должность"
value={task.sToClnpsdep}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sToClnperson"
label="Сотрудник"
value={task.sToClnperson}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sToFcstaffgrp"
label="Нештатная должность"
value={task.sToFcstaffgrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sToUser" label="Пользователь" value={task.sToUser} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToUsergrp"
label="Группа пользователей"
value={task.sToUsergrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации об исполнителе
TaskFormTabExecutor.propTypes = {
task: PropTypes.object.isRequired,
onFieldEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabExecutor };

View File

@ -1,192 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка основной информации
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_FEW_COLUMNS: { display: "flex", flexWrap: "wrap", justifyContent: "space-between" }
};
//-----------
//Тело модуля
//-----------
//Вкладка основной информации
const TaskFormTabInfo = ({ task, editable, onFieldEdit, onEventNextNumbGet }) => {
//Вспомогательные функции открытия раздела
const { handleClientPersonOpen, handleCatalogTreeOpen, handleClientClientsOpen } = useDictionary();
//При изменении каталога
const handleCrnChange = () =>
handleCatalogTreeOpen({
sUnitName: "ClientEvents",
sName: task.sCrn,
callBack: res => {
onFieldEdit({
target: {
id: "sCrn",
value: res.outParameters.out_NAME
}
});
}
});
//При изменении клиента-сотрудника
const handleClntClnpersonChange = () =>
handleClientPersonOpen({
sCode: task.sClntClnperson,
callBack: res => {
onFieldEdit({
target: {
id: "sClntClnperson",
value: res.outParameters.out_CODE
}
});
}
});
//При изменении клиента-организации
const handleClntClientsChange = () =>
handleClientClientsOpen({
sCode: task.sClntClients,
callBack: res => {
onFieldEdit({
target: {
id: "sClntClients",
value: res.outParameters.out_CLIENT_CODE
}
});
}
});
//Генерация содержимого
return (
<Box>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Событие</legend>
<Box sx={STYLES.BOX_FEW_COLUMNS}>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sCrn"
label="Каталог"
fullWidth
value={task.sCrn}
variant="standard"
onChange={onFieldEdit}
InputProps={getInputProps(handleCrnChange)}
required
disabled={task.isUpdate}
/>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px")}
id="sPrefix"
label="Префикс"
value={task.sPrefix}
variant="standard"
onChange={onFieldEdit}
required
disabled={task.isUpdate}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px")}
id="sNumber"
label="Номер"
value={task.sNumber}
variant="standard"
onChange={onFieldEdit}
required
disabled={task.isUpdate}
InputProps={getInputProps(onEventNextNumbGet, !task.sPrefix || task.isUpdate, "refresh")}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px", !task.isUpdate)}
id="sType"
label="Тип"
value={task.sType}
variant="standard"
onChange={onFieldEdit}
disabled
required
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px", !task.isUpdate)}
id="sStatus"
label="Статус"
value={task.sStatus}
variant="standard"
disabled
required
onChange={onFieldEdit}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
fullWidth
id="sDescription"
label="Описание"
value={task.sDescription}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType || !editable}
required
multiline
minRows={7}
maxRows={7}
></TextField>
</Box>
</Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Клиент</legend>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sClntClients"
label="Организация"
value={task.sClntClients}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType}
InputProps={getInputProps(() => handleClntClientsChange(), !task.sType)}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sClntClnperson"
label="Сотрудник"
value={task.sClntClnperson}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType}
InputProps={getInputProps(() => handleClntClnpersonChange(), !task.sType)}
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка основной информации
TaskFormTabInfo.propTypes = {
task: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
onFieldEdit: PropTypes.func.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabInfo };

View File

@ -1,169 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации со свойствами
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
import { DP_DEFAULT_VALUE, DP_IN_VALUE, DP_RETURN_VALUE, validationError, formatSqlDate } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации со свойствами
const TaskFormTabProps = ({ task, docProps, onPropEdit }) => {
//Вспомогательные функции открытия раздела
const { handleExtraDictionariesOpen, handleUnitOpen } = useDictionary();
//Выбор из словаря или дополнительного словаря
const handleDictOpen = async (docProp, curValue = null) => {
//Если способ выбора - словарь
docProp.NENTRY_TYPE === 1
? handleUnitOpen({
sUnitCode: docProp.SUNITCODE,
sShowMethod: docProp.SMETHOD_CODE,
inputParameters: docProp.NPARAM_RN ? [{ name: docProp.SPARAM_IN_CODE, value: curValue }] : null,
callBack: res => {
onPropEdit(docProp.SFORMATTED_ID, res.outParameters[docProp.SPARAM_OUT_CODE]);
}
})
: //Если способ выбора - доп. словарь
handleExtraDictionariesOpen({
nRn: docProp.NEXTRA_DICT_RN,
sParamName: DP_IN_VALUE[docProp.NFORMAT],
paramValue: curValue,
callBack: res => {
onPropEdit(docProp.SFORMATTED_ID, res.outParameters[DP_RETURN_VALUE[docProp.NFORMAT]]);
}
});
};
//Инициализация дополнительного свойства
const initPropValue = prop => {
//Считываем значение свойства из события
const value = task.docProps[prop.SFORMATTED_ID];
//Если есть значение свойства
if (value) {
//Строка или число
if (prop.NFORMAT < 2) {
return prop.NNUM_PRECISION ? String(value).replace(".", ",") : value;
}
//Дата
if (prop.NFORMAT === 2) {
//Возвращаем значение исходя из подтипа даты
switch (prop.NDATA_SUBTYPE) {
//Дата без времени
case 0:
return dayjs(value).format("YYYY-MM-DD");
//Дата и время без секунд
case 1:
return dayjs(value).format("YYYY-MM-DD HH:mm");
//Дата и время с секундами
default:
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
}
//Если это ничего из вышестоящего - время
return formatSqlDate(value);
}
//Если нет значения, но это изменение события
if (task.nRn) {
//Возвращаем пустоту
return "";
}
//Если нет значения и это добавление события - возвращаем значение по умолчанию
return prop[DP_DEFAULT_VALUE[prop.NFORMAT]];
};
//Генерация содержимого
return (
<Box>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
{docProps.props.map((docProp, index) => {
return docProp.BSHOW_IN_GRID ? (
<TextField
error={
!validationError(
task.docProps[docProp.SFORMATTED_ID],
docProp.NFORMAT,
docProp.NNUM_WIDTH,
docProp.NNUM_PRECISION,
docProp.NSTR_WIDTH
)
}
key={index}
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id={docProp.SFORMATTED_ID}
type={
docProp.NFORMAT < 2
? "string"
: docProp.NFORMAT === 2
? docProp.NDATA_SUBTYPE === 0
? "date"
: "datetime-local"
: "time"
}
label={docProp.SNAME}
fullWidth
value={initPropValue(docProp)}
variant="standard"
onChange={e => onPropEdit(e.target.id, e.target.value)}
inputProps={
(docProp.NFORMAT === 2 && docProp.NDATA_SUBTYPE === 2) || (docProp.NFORMAT === 3 && docProp.NDATA_SUBTYPE === 1)
? { step: 1 }
: {}
}
InputProps={
docProp.NENTRY_TYPE > 0 ? getInputProps(() => handleDictOpen(docProp, task.docProps[docProp.SFORMATTED_ID])) : null
}
InputLabelProps={
docProp.NFORMAT < 2
? {}
: {
shrink: true
}
}
required={docProp.BREQUIRE}
disabled={docProp.BREADONLY}
/>
) : null;
})}
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации со свойствами
TaskFormTabProps.propTypes = {
task: PropTypes.object.isRequired,
docProps: PropTypes.object.isRequired,
onPropEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabProps };

View File

@ -1,212 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Фильтр отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Chip, Stack, Icon, IconButton, Box, Menu, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { FilterDialog } from "./components/filter_dialog.js"; //Диалог фильтра
import { COMMON_STYLES } from "./styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
ICON_ORDERS: orders => {
return orders.length > 0 ? { color: "#1976d2" } : {};
},
MENU_ORDER: {
width: "260px"
},
MENU_ITEM_ORDER: {
display: "flex",
justifyContent: "space-between"
},
FILTERS_STACK: {
paddingBottom: "5px",
...COMMON_STYLES.SCROLL
},
STACK_FILTER: { maxWidth: "99vw" }
};
//--------------------------
//Вспомогательные компоненты
//--------------------------
//Элемент меню сортировок
const SortMenuItem = ({ item, caption, orders, onOrderChanged }) => {
//Кнопка сортировки
const order = orders.find(order => order.name == item);
//Генерация содержимого
return (
<MenuItem sx={STYLES.MENU_ITEM_ORDER} key={item} onClick={() => onOrderChanged(item)}>
<Typography>{caption}</Typography>
{order ? order.direction === "ASC" ? <Icon>arrow_upward</Icon> : <Icon>arrow_downward</Icon> : null}
</MenuItem>
);
};
//Контроль свойств компонента - Элемент меню сортировок
SortMenuItem.propTypes = {
item: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//Меню сортировок
const SortMenu = ({ menuOrders, onOrdersMenuClose, orders, onOrderChanged }) => {
//Генерация содержимого
return (
<Menu
id={`sort_menu`}
anchorEl={menuOrders.anchorMenuOrders}
open={menuOrders.openOrders}
onClose={onOrdersMenuClose}
MenuListProps={{ sx: STYLES.MENU_ORDER }}
>
<SortMenuItem item={"DCHANGE_DATE"} caption={"Дата последнего изменения"} orders={orders} onOrderChanged={onOrderChanged} />
<SortMenuItem item={"DPLAN_DATE"} caption={"Дата начала работ"} orders={orders} onOrderChanged={onOrderChanged} />
<SortMenuItem item={"SPREF_NUMB"} caption={"Номер"} orders={orders} onOrderChanged={onOrderChanged} />
</Menu>
);
};
//Контроль свойств компонента - Меню сортировок
SortMenu.propTypes = {
menuOrders: PropTypes.object.isRequired,
onOrdersMenuClose: PropTypes.func.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//Элемент фильтра
const FilterItem = ({ caption, value, onClick }) => {
//При нажатии на элемент
const handleClick = () => (onClick ? onClick() : null);
//Генерация содержимого
return (
<Chip
label={
<Stack direction={"row"} alignItems={"center"}>
<strong>{caption}</strong>
{value ? `:\u00A0${value}` : null}
</Stack>
}
variant="outlined"
onClick={handleClick}
/>
);
};
//Контроль свойств компонента - Элемент фильтра
FilterItem.propTypes = {
caption: PropTypes.string.isRequired,
value: PropTypes.any,
onClick: PropTypes.func
};
//---------------
//Тело компонента
//---------------
//Фильтр отбора
const Filter = ({
isFilterDialogOpen,
filter,
docLinks,
selectedDocLink,
onFilterChange,
onDocLinksLoad,
onFilterOpen,
onFilterClose,
onTasksReload,
orders,
onOrderChanged,
...other
}) => {
//Состояние меню сортировки
const [menuOrders, setMenuOrders] = useState({ anchorMenuOrders: null, openOrders: false });
//При нажатии на открытие меню сортировки
const handleOrdersMenuButtonClick = event => {
setMenuOrders(pv => ({ ...pv, anchorMenuOrders: event.currentTarget, openOrders: true }));
};
//При закрытии меню
const handleOrdersMenuClose = () => {
setMenuOrders(pv => ({ ...pv, anchorMenuOrders: null, openOrders: false }));
};
//Генерация содержимого
return (
<div>
{isFilterDialogOpen ? (
<FilterDialog
initial={{ filter, docLinks }}
// docLinks={docLinks}
onFilterChange={onFilterChange}
onFilterClose={onFilterClose}
onDocLinksLoad={onDocLinksLoad}
/>
) : null}
<Box {...other}>
<Stack direction="row" spacing={1} p={1} alignItems={"center"} sx={STYLES.STACK_FILTER}>
<IconButton title="Обновить" onClick={onTasksReload}>
<Icon>refresh</Icon>
</IconButton>
<IconButton title="Сортировать" sx={STYLES.ICON_ORDERS(orders)} onClick={handleOrdersMenuButtonClick}>
<Icon>sort</Icon>
</IconButton>
<IconButton title="Фильтр" onClick={onFilterOpen}>
<Icon>filter_alt</Icon>
</IconButton>
<Stack direction="row" spacing={1} alignItems={"center"} sx={STYLES.FILTERS_STACK}>
{filter.sState ? <FilterItem caption={"Состояние"} value={filter.sState} onClick={onFilterOpen} /> : null}
{filter.sType ? <FilterItem caption={"Тип"} value={filter.sType} onClick={onFilterOpen} /> : null}
{filter.sCrnName ? <FilterItem caption={"Каталог"} value={filter.sCrnName} onClick={onFilterOpen} /> : null}
{filter.bSubcatalogs ? <FilterItem caption={"Включая подкаталоги"} onClick={onFilterOpen} /> : null}
{filter.sSendPerson ? <FilterItem caption={"Исполнитель"} value={filter.sSendPerson} onClick={onFilterOpen} /> : null}
{filter.sSendDivision ? <FilterItem caption={"Подразделение"} value={filter.sSendDivision} onClick={onFilterOpen} /> : null}
{filter.sSendUsrGrp ? (
<FilterItem caption={"Группа пользователей"} value={filter.sSendUsrGrp} onClick={onFilterOpen} />
) : null}
{filter.sDocLink && selectedDocLink ? (
<FilterItem caption={"Учётный документ"} value={selectedDocLink.descr} onClick={onFilterOpen} />
) : null}
</Stack>
</Stack>
<SortMenu menuOrders={menuOrders} onOrdersMenuClose={handleOrdersMenuClose} orders={orders} onOrderChanged={onOrderChanged} />
</Box>
</div>
);
};
//Контроль свойств компонента - Фильтр отбора
Filter.propTypes = {
isFilterDialogOpen: PropTypes.bool.isRequired,
filter: PropTypes.object.isRequired,
docLinks: PropTypes.arrayOf(PropTypes.object),
selectedDocLink: PropTypes.object,
onFilterChange: PropTypes.func.isRequired,
onDocLinksLoad: PropTypes.func,
onFilterOpen: PropTypes.func.isRequired,
onFilterClose: PropTypes.func.isRequired,
onTasksReload: PropTypes.func.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { Filter };

View File

@ -1,255 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки открытия разделов
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
//-----------
//Тело модуля
//-----------
//Состояние открытия разделов
const useDictionary = () => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Отображение раздела "Сотрудники"
const handleClientPersonOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientPersons",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Клиенты"
const handleClientClientsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientClients",
showMethod: "main",
inputParameters: [{ name: "in_CLIENT_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Каталоги"
const handleCatalogTreeOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "CatalogTree",
showMethod: "main",
inputParameters: [
{ name: "in_DOCNAME", value: prms.sUnitName },
{ name: "in_NAME", value: prms.sName }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Типы событий"
const handleEventTypesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEventTypes",
showMethod: "dictionary",
inputParameters: [{ name: "pos_eventtypecode", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Контрагенты"
const handleAgnlistOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "AGNLIST",
showMethod: "agents",
inputParameters: [{ name: "pos_agnmnemo", value: prms.sMnemo }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Штатные подразделения"
const handleInsDepartmentOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "INS_DEPARTMENT",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Нештатные структуры"
const handleCostStaffGroupsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "CostStaffGroups",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Дополнительные словари"
const handleExtraDictionariesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ExtraDictionaries",
showMethod: "values",
inputParameters: [
{ name: "pos_rn", value: prms.nRn },
{ name: prms.sParamName, value: prms.paramValue }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Маршруты событий (исполнители в точках)"
const handleEventRoutesPointExecutersOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "EventRoutesPointExecuters",
showMethod: "executers",
inputParameters: prms.inputParameters,
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "События"
const handleClientEventsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEvents",
inputParameters: [{ name: "in_Ident", value: prms.nIdent }]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "События (примечания)"
const handleClientEventsNotesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEventsNotes",
showMethod: "main",
inputParameters: [{ name: "in_PRN", value: prms.nPrn }]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Присоединенные документы"
const handleFileLinksOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "FileLinks",
showMethod: "main_link",
inputParameters: [
{ name: "in_PRN", value: prms.nPrn },
{ name: "in_UNITCODE", value: prms.sUnitCode }
]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Маршруты событий (точки перехода)"
const handleEventRoutesPointsPassessOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "EventRoutesPointsPasses",
showMethod: "main_passes",
inputParameters: [
{ name: "in_ENVTYPE_CODE", value: prms.sEventType },
{ name: "in_ENVSTAT_CODE", value: prms.sEventStatus },
{ name: "in_POINT", value: prms.nPoint }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Универсальное отображение раздела
const handleUnitOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: prms.sUnitCode,
showMethod: prms.sShowMethod,
inputParameters: prms.inputParameters,
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
return {
handleClientPersonOpen,
handleClientClientsOpen,
handleCatalogTreeOpen,
handleEventTypesOpen,
handleAgnlistOpen,
handleInsDepartmentOpen,
handleCostStaffGroupsOpen,
handleExtraDictionariesOpen,
handleEventRoutesPointExecutersOpen,
handleClientEventsOpen,
handleClientEventsNotesOpen,
handleFileLinksOpen,
handleEventRoutesPointsPassessOpen,
handleUnitOpen
};
};
//----------------
//Интерфейс модуля
//----------------
export { useDictionary };

View File

@ -1,122 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки фильтра
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useEffect, useCallback } from "react"; //Классы React
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
import { getLocalStorageValue } from "../layouts"; //Вспомогательные функции
//--------------------------
//Вспомогательные компоненты
//--------------------------
//Проверка возможности загрузки данных фильтра из локального хранилища
const isLocalStorageExists = () => {
return getLocalStorageValue("sType");
};
//-----------
//Тело модуля
//-----------
//Хук фильтра
//const useFilters = filterOpen => {
const useFilters = () => {
//Состояние фильтра
const [filters, setFilters] = useState({
loaded: false,
isSetByUser: !isLocalStorageExists(),
values: {
sState: EVENT_STATES[1],
sType: "",
sCrnName: "",
sCrnRnList: "",
bSubcatalogs: false,
sSendPerson: "",
sSendDivision: "",
sSendUsrGrp: "",
sDocLink: ""
}
});
//Установить значение фильтра
const setFilterValues = useCallback((values, isSetByUser = true) => {
setFilters({ loaded: true, isSetByUser: isSetByUser, values: values });
}, []);
//Загрузка значений фильтра из локального хранилища браузера
const loadLocalStorageValues = useCallback(async () => {
//Загружаем значения по умолчанию
let values = { ...filters.values };
//Обходим ключи объекта значений
for (let key in values) {
//Заполняем значениями из хранилища
switch (key) {
//Локальное хранилище не хранит булево, форматируем строку в булево
case "bSubcatalogs":
values[key] = getLocalStorageValue(key) === "true";
break;
//Не переносим информацию о связанных записях
case "sDocLink":
break;
//Переносим все остальные значения
default:
values[key] = getLocalStorageValue(key, "");
break;
}
}
//Устанавливаем значения фильтра
setFilterValues(values, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//При изменении значений фильтра
const handleFiltersChange = useCallback(
filters => {
setFilterValues(filters);
},
[setFilterValues]
);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
//Обходим ключи фильтра
for (let key in filters.values) {
//Если это не связи - сохраняем значение в хранилище
key !== "sDocLink" ? localStorage.setItem(key, filters.values[key] ? filters.values[key] : "") : null;
}
};
//Если данные были загружены и произошли изменения
if (filters.loaded && filters.isSetByUser) {
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
}
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [filters.loaded, filters.isSetByUser, filters.values]);
//При подключении к странице
useEffect(() => {
//Если требуется загрузить фильтр из локального хранилища
if (!filters.loaded && !filters.isSetByUser) {
loadLocalStorageValues();
}
}, [filters.isSetByUser, filters.loaded, loadLocalStorageValues]);
return [filters, handleFiltersChange];
};
//----------------
//Интерфейс модуля
//----------------
export { useFilters };

View File

@ -1,243 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки основных данных
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { getRandomColor, getLocalStorageValue } from "../layouts"; //Вспомогательные функции
//-----------
//Тело модуля
//-----------
//Хук дополнительных данных
const useExtraData = filtersType => {
//Состояние дополнительных данных
const [extraData, setExtraData] = useState({
dataLoaded: false,
reload: false,
typeLoaded: "",
evRoutes: [],
evPoints: [],
noteTypes: [],
docLinks: []
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Считывание учётных документов
const handleDocLinksLoad = useCallback(
async (type = filtersType) => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DOCLINKS_GET",
args: {
SEVNTYPE_CODE: type
},
isArray: name => name === "XDOCLINKS",
respArg: "COUT"
});
//Возвращаем учётные документы
return [...(data?.XDOCLINKS || [])];
},
[executeStored, filtersType]
);
useEffect(() => {
//Загрузка дополнительных данных
const loadExtraData = async () => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_GET_INFO_BY_CODE",
args: {
SEVNTYPE_CODE: filtersType
},
isArray: name => ["XEVROUTES", "XEVPOINTS", "XNOTETYPES"].includes(name),
respArg: "COUT"
});
//Форматируем типы примечаний под нужный формат
let noteTypes = [...(data?.XNOTETYPES || [])].reduce((prev, cur) => [...prev, cur.SNAME], []);
//Считываем учётные документы
let docLinks = await handleDocLinksLoad(filtersType);
//Обновляем дополнительные данные
setExtraData({
dataLoaded: true,
reload: false,
typeLoaded: filtersType,
evRoutes: [...(data?.XEVROUTES || [])],
evPoints: [...(data?.XEVPOINTS || [])],
noteTypes: [...noteTypes],
docLinks: [...docLinks]
});
};
//Если указан тип событий и необходимо обновить
if (extraData.reload && filtersType) {
//Загружаем дополнительные данные
if (!extraData.typeLoaded || filtersType !== extraData.typeLoaded) {
loadExtraData();
}
}
}, [executeStored, extraData.reload, extraData.typeLoaded, filtersType, handleDocLinksLoad]);
return [extraData, setExtraData, handleDocLinksLoad];
};
//Хук заливок пользовательских настроек
const useColorRules = () => {
//Собственное состояние
const [colorRules, setColorRules] = useState({
loaded: false,
rules: [],
selectedColorRule: JSON.parse(getLocalStorageValue("settingsColorRule") || {})
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости загрузки заливок
useEffect(() => {
//Считывание пользовательских настроек
let getColorRules = async () => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DP_RULES_GET",
isArray: name => name === "XRULES",
respArg: "COUT"
});
//Формируем массив правил заливки пользовательских настроек
let newColorRules = [...(data.XRULES || [])].reduce(
(prev, cur) => [
...prev,
{
id: prev.length,
SFIELD: cur.SFIELD,
SDP_NAME: cur.SDP_NAME,
SCOLOR: cur.SCOLOR,
STYPE: cur.STYPE,
fromValue: cur.NFROM ?? cur.SFROM ?? cur.DFROM,
toValue: cur.NTO ?? cur.STO ?? cur.DTO
}
],
[]
);
//Устанавливаем заливки пользовательских настроек
setColorRules(pv => ({ ...pv, loaded: true, rules: [...newColorRules] }));
};
if (!colorRules.loaded) getColorRules();
}, [colorRules.loaded, executeStored]);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
localStorage.setItem("settingsColorRule", JSON.stringify(colorRules.selectedColorRule));
};
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [colorRules.selectedColorRule]);
return [colorRules, setColorRules];
};
//Хук статусов событий
const useStatuses = filterType => {
//Собственное состояние статусов
const [statuses, setStatuses] = useState([]);
//Состояние статусов
const [statusesState, setStatusesState] = useState({
sorted: false,
reload: true,
attr: getLocalStorageValue("statusesSortAttr", "SEVNSTAT_NAME"),
direction: getLocalStorageValue("statusesSortDirection", "asc")
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости сортировки статусов
useEffect(() => {
//Сортируем статусы
const sortStatuses = unsortedStatuses => {
//Инициализируем поле сортировки и порядок сортировки
const attr = statusesState.attr;
const direction = statusesState.direction;
//Сортируем
let sortedStatuses = unsortedStatuses.sort((a, b) =>
direction === "asc" ? a[attr].localeCompare(b[attr]) : b[attr].localeCompare(a[attr])
);
//Возвращаем
return sortedStatuses;
};
//Загружаем и сортируем статусы
const loadAndSortStatuses = async filterType => {
//Инициализируем статусы
let newStatuses = [];
//Если требуется перезагрузка
if (statusesState.reload) {
const loadedStatuses = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVNSTATS_LOAD",
args: {
SCLNEVNTYPES: filterType
},
isArray: name => name === "XSTATUS",
respArg: "COUT"
});
//Загружаем статусы и инициализируем цвета
newStatuses = [...(loadedStatuses?.XSTATUS || [])].reduce(
(prev, cur) => [...prev, { ...cur, color: getRandomColor(prev.length + 1) }],
[]
);
} else {
//Загружаем из состояния
newStatuses = [...statuses];
}
//Сортируем, если требуется
newStatuses = !statusesState.sorted ? sortStatuses(newStatuses) : newStatuses;
//Обновляем состояние статусов
setStatuses([...newStatuses]);
//Обновляем информацию о состоянии статусов
setStatusesState(pv => ({ ...pv, sorted: true, reload: false }));
};
//При необходимости изменения сортировки
if (filterType && (statusesState.reload || !statusesState.sorted)) {
//Считываем старые статусы или загружаем новые
loadAndSortStatuses(filterType);
}
}, [executeStored, filterType, statuses, statusesState.attr, statusesState.direction, statusesState.reload, statusesState.sorted]);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
localStorage.setItem("statusesSortAttr", statusesState.attr);
localStorage.setItem("statusesSortDirection", statusesState.direction);
};
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [statusesState.attr, statusesState.direction]);
return [statuses, statusesState, setStatuses, setStatusesState];
};
//----------------
//Интерфейс модуля
//----------------
export { useExtraData, useColorRules, useStatuses };

View File

@ -1,191 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки диалога события
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Хук для события
const useClientEvent = (taskRn, taskType = "", taskStatus = "") => {
//Собственное состояние
const [task, setTask] = useState({
init: true,
nRn: taskRn,
sCrn: "",
sPrefix: "",
sNumber: "",
sType: taskType,
sStatus: taskStatus,
sDescription: "",
sClntClients: "",
sClntClnperson: "",
dStartDate: "",
sInitClnperson: "",
sInitUser: "",
sInitReason: "",
sToCompany: "",
sToDepartment: "",
sToClnpost: "",
sToClnpsdep: "",
sToClnperson: "",
sToFcstaffgrp: "",
sToUser: "",
sToUsergrp: "",
sCurrentUser: "",
isUpdate: false,
insertDisabled: true,
updateDisabled: true,
docProps: {}
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При инициализации события
useEffect(() => {
//Если это инициализация
if (task.init) {
//Если указан рег. номер события
if (taskRn) {
//Считывание параметров события
const readEvent = async () => {
//Считываем информацию о событии по рег. номеру
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_GET",
args: {
NCLNEVENTS: task.nRn
},
respArg: "COUT"
});
//Фильтруем доп. свойства
let docProps = Object.keys(data.XEVENT)
.filter(key => key.includes("DP_"))
.reduce((prev, key) => ({ ...prev, [key]: data.XEVENT[key] }), {});
//Устанавливаем информацию о событии
setTask(pv => ({
...pv,
sCrn: data.XEVENT.SCRN,
sPrefix: data.XEVENT.SPREF,
sNumber: data.XEVENT.SNUMB,
sType: data.XEVENT.STYPE,
sStatus: data.XEVENT.SSTATUS,
sDescription: data.XEVENT.SDESCRIPTION,
sClntClients: data.XEVENT.SCLIENT_CLIENT,
sClntClnperson: data.XEVENT.SCLIENT_PERSON,
dPlanDate: data.XEVENT.SPLAN_DATE,
sInitClnperson: data.XEVENT.SINIT_PERSON,
sInitUser: data.XEVENT.SINIT_AUTHID,
sInitReason: data.XEVENT.SREASON,
sToCompany: data.XEVENT.SSEND_CLIENT,
sToDepartment: data.XEVENT.SSEND_DIVISION,
sToClnpost: data.XEVENT.SSEND_POST,
sToClnpsdep: data.XEVENT.SSEND_PERFORM,
sToClnperson: data.XEVENT.SSEND_PERSON,
sToFcstaffgrp: data.XEVENT.SSEND_STAFFGRP,
sToUser: data.XEVENT.SSEND_USER_NAME,
sToUsergrp: data.XEVENT.SSEND_USER_GROUP,
sCurrentUser: data.XEVENT.SINIT_AUTHID,
isUpdate: true,
init: false,
docProps: docProps
}));
};
//Инициализация параметров события
readEvent();
} else {
//Считывание изначальных параметров события
const initEvent = async () => {
//Инициализируем параметры события
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INIT",
args: {
SEVENT_TYPE: task.sType
}
});
//Если есть данные
if (data) {
//Устанавливаем данные по событию
setTask(pv => ({
...pv,
sPrefix: data.SPREF,
sNumber: data.SNUMB,
sCurrentUser: data.SINIT_AUTHNAME,
sInitClnperson: data.SINIT_PERSON,
sInitUser: !data.SINIT_PERSON ? data.SINIT_AUTHNAME : "",
init: false
}));
}
};
//Инициализация изначальных параметров события
initEvent();
}
}
if (!task.init) {
setTask(pv => ({ ...pv, sInitUser: !task.sInitClnperson ? task.sCurrentUser : "" }));
}
}, [executeStored, task.init, task.nRn, task.sType, task.sCurrentUser, task.sInitClnperson, taskRn]);
//Проверка доступности действия
useEffect(() => {
setTask(pv => ({
...pv,
insertDisabled:
!task.sCrn ||
!task.sPrefix ||
!task.sNumber ||
!task.sType ||
!task.sStatus ||
!task.sDescription ||
(!task.sInitClnperson && !task.sInitUser),
updateDisabled: !task.sDescription
}));
}, [task.sCrn, task.sDescription, task.sInitClnperson, task.sInitUser, task.sNumber, task.sPrefix, task.sStatus, task.sType]);
return [task, setTask];
};
//Хук для получения свойств раздела "События"
const useDocsProps = taskType => {
//Собственное состояние
const [docProps, setDocsProps] = useState({ loaded: false, props: [] });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
useEffect(() => {
//Загрузка доп. свойств
let getDocsProps = async () => {
//Считываема доп. свойства по типу события
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_PROPS_GET",
args: { SEVNTYPE_CODE: taskType },
isArray: name => name === "XPROPS",
respArg: "COUT"
});
//Устанавливаем доп. свойства
setDocsProps({ loaded: true, props: [...(data?.XPROPS || [])] });
};
//Если доп. свойства не загружены
if (!docProps.loaded) {
//Загружаем доп. свойства
getDocsProps();
}
}, [docProps.loaded, executeStored, taskType]);
return [docProps];
};
//----------------
//Интерфейс модуля
//----------------
export { useClientEvent, useDocsProps };

View File

@ -1,431 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки событий
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../core/utils"; //Вспомогательные функции
import { convertFilterValuesToArray } from "../layouts"; //Вспомогательные функции
import { useDictionary } from "./dict_hooks"; //Состояние открытия разделов
//-----------
//Тело модуля
//-----------
//Хук обработки перехода события
const useTasksFunctions = () => {
//Состояние открытия раздела
const { handleEventRoutesPointExecutersOpen, handleEventRoutesPointsPassessOpen } = useDictionary();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Выполнение направления события
const handleSendExec = useCallback(
//Выполняем финальное перенаправление события
async ({ mainArgs, onReload = null }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: { ...mainArgs }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При направлении события
const handleSend = useCallback(
async ({ mainArgs, onReload = null, onNoteOpen = null }) => {
//Если требуется добавить примечание
if (onNoteOpen) {
//Открываем примечание с коллбэком на направление события
onNoteOpen(async note => {
//Выполняем изменение статуса
handleSendExec({ mainArgs: { ...mainArgs, SNOTE_HEADER: note.header, SNOTE: note.text }, onReload });
});
} else {
//Выполняем изменение статуса
handleSendExec({ mainArgs, onReload });
}
},
[handleSendExec]
);
//По нажатию действия "Направить"
const handleTaskSend = useCallback(
async ({ nEvent, onReload = null, onNoteOpen = null }) => {
//Выполняем инициализацию параметров
const firstStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: {
NSTEP: 1,
NEVENT: nEvent
}
});
if (firstStep) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
handleEventRoutesPointExecutersOpen({
inputParameters: [
{ name: "in_IDENT", value: firstStep.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_PERSON_CODE", value: firstStep.SSEND_PERSON },
{ name: "in_USER_NAME", value: firstStep.SSEND_USER_NAME },
{ name: "in_EVENT_TYPE", value: firstStep.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: firstStep.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: firstStep.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: firstStep.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: firstStep.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: firstStep.SCLIENT_PERSON }
],
callBack: sendPrms => {
//Собираем основные параметры направления события
const mainArgs = {
NIDENT: firstStep.NIDENT,
NSTEP: 2,
NEVENT: nEvent,
SSEND_CLIENT: sendPrms.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: sendPrms.outParameters.out_DIVISION_CODE,
SSEND_POST: sendPrms.outParameters.out_POST_CODE,
SSEND_PERFORM: sendPrms.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: sendPrms.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: sendPrms.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: sendPrms.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: sendPrms.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: sendPrms.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: sendPrms.outParameters.out_PREDEFINED_PROC
};
//Перенаправляем событие
handleSend({ nEvent, mainArgs, onReload, onNoteOpen });
}
});
}
},
[executeStored, handleEventRoutesPointExecutersOpen, handleSend]
);
//Выполнение изменения статуса события
const handleStateChangeExec = useCallback(
async ({ mainArgs, onReload = null }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: { ...mainArgs }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При изменении статуса события
const handleStateChange = useCallback(
async ({ mainArgs, onReload = null, onNoteOpen = null }) => {
//Если необходимо добавить примечание
if (onNoteOpen) {
//Открываем примечание с коллбэком на изменение статуса
onNoteOpen(async note => {
//Выполняем изменение статуса
handleStateChangeExec({ mainArgs: { ...mainArgs, SNOTE_HEADER: note.header, SNOTE: note.text }, onReload });
});
} else {
//Выполняем изменение статуса
handleStateChangeExec({ mainArgs, onReload });
}
},
[handleStateChangeExec]
);
//При выборе исполнителя
const handleExecuterSelect = useCallback(
async ({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Если требуется выбрать получателя
if (pointInfo.NSELECT_EXEC === 1) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
handleEventRoutesPointExecutersOpen({
inputParameters: [
{ name: "in_IDENT", value: pointInfo.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_EVENT_TYPE", value: pointInfo.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: pointInfo.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: pointInfo.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: pointInfo.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: pointInfo.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: pointInfo.SCLIENT_PERSON }
],
callBack: sendPrms => {
const mainArgs = {
NIDENT: pointInfo.NIDENT,
NSTEP: 3,
NEVENT: nEvent,
SEVENT_STAT: pointInfo.SEVENT_STAT,
SSEND_CLIENT: sendPrms.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: sendPrms.outParameters.out_DIVISION_CODE,
SSEND_POST: sendPrms.outParameters.out_POST_CODE,
SSEND_PERFORM: sendPrms.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: sendPrms.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: sendPrms.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: sendPrms.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: sendPrms.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: sendPrms.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: sendPrms.outParameters.out_PREDEFINED_PROC
};
//Выполняем изменение статуса
handleStateChange({ mainArgs, onReload, onNoteOpen });
}
});
} else {
//Общие аргументы
const mainArgs = {
NIDENT: pointInfo.NIDENT,
NSTEP: 3,
NEVENT: nEvent,
SEVENT_STAT: pointInfo.SEVENT_STAT
};
//Выполняем изменение статуса
handleStateChange({ mainArgs, onReload, onNoteOpen });
}
},
[handleEventRoutesPointExecutersOpen, handleStateChange]
);
//При выполнении второго шага
const handleMakeSecondStep = useCallback(
async ({ nIdent, nPass }) => {
//Выполняем переход на следующий шаг
const secondStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NIDENT: nIdent,
NSTEP: 2,
NPASS: nPass
}
});
//Возвращаем параметры выполнения
return secondStep;
},
[executeStored]
);
//При выборе следующей точки события
const handleNextPointSelect = useCallback(
({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Открываем раздел "Маршруты событий (точки перехода)" для выбора следующей точки
handleEventRoutesPointsPassessOpen({
sEventType: pointInfo.SEVENT_TYPE,
sEventStatus: pointInfo.SEVENT_STAT,
nPoint: pointInfo.NPOINT,
callBack: async point => {
//Выполняем второй шаг
let secondStep = await handleMakeSecondStep({ nIdent: pointInfo.NIDENT, nPass: point.outParameters.out_RN });
//Выполняем выбор исполнителя
handleExecuterSelect({
nEvent,
pointInfo: { ...pointInfo, SEVENT_STAT: point.outParameters.out_NEXT_POINT, NSELECT_EXEC: secondStep.NSELECT_EXEC },
onReload,
onNoteOpen
});
}
});
},
[handleEventRoutesPointsPassessOpen, handleMakeSecondStep, handleExecuterSelect]
);
//По нажатию действия "Перейти"
const handleTaskStateChange = useCallback(
async ({ nEvent, sNextStat = null, onReload = null, onNoteOpen = null }) => {
//Выполняем инициализацию параметров
const eventInfo = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NSTEP: 1,
NEVENT: nEvent,
SNEXT_STAT: sNextStat
}
});
//Если информация о события проинициализирована
if (eventInfo) {
//Если следующий статус неопределен
if (!sNextStat) {
//Выполнение перехода с выбором точки
handleNextPointSelect({
nEvent,
pointInfo: eventInfo,
onReload,
onNoteOpen
});
} else {
//Выполняем перехода без выбора точки
handleExecuterSelect({
nEvent,
pointInfo: eventInfo,
onReload,
onNoteOpen
});
}
}
},
[executeStored, handleExecuterSelect, handleNextPointSelect]
);
return { handleTaskStateChange, handleTaskSend };
};
//Хук получения событий
const useTasks = (filterValues, ordersValues) => {
//Состояние событий
const [tasks, setTasks] = useState({
loaded: false,
rows: [],
reload: false,
accountsReload: false,
loadedAccounts: []
});
//Состояние вспомогательных функций событий
const { handleTaskStateChange } = useTasksFunctions();
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Инициализация параметров события
const initTask = (id, task, avatar = null) => {
//Фильтруем доп. свойства
let newDocProps = Object.keys(task)
.filter(key => key.includes("DP_"))
.reduce((prev, key) => ({ ...prev, [key]: task[key] }), {});
//Возвращаем структуру события
return {
id: id,
avatar: avatar,
name: task.SPREF_NUMB,
nRn: task.NRN,
sCrn: "",
sPrefix: task.SEVPREF,
sNumber: task.SEVNUMB,
sType: task.SEVTYPE_CODE,
sStatus: task.SEVSTAT_NAME,
sDescription: task.SEVDESCR,
sClntClients: "",
sClntClnperson: "",
dchange_date: task.DCHANGE_DATE,
dStartDate: task.DREG_DATE,
dExpireDate: task.DEXPIRE_DATE,
dPlanDate: task.DPLAN_DATE,
sInitClnperson: task.SINIT_PERSON,
sInitUser: "",
sInitReason: "",
sToCompany: "",
sToDepartment: task.SSEND_DIVISION,
sToClnpost: "",
sToClnpsdep: "",
sToClnperson: task.SSEND_PERSON,
sToFcstaffgrp: "",
sToUser: "",
sToUsergrp: task.SSEND_USRGRP,
sSender: task.SSENDER,
sCurrentUser: "",
sLinkedUnit: task.SLINKED_UNIT,
nLinkedRn: task.NLINKED_RN,
docProps: newDocProps
};
};
//Взаимодействие с событием (через перенос)
const onDragEnd = useCallback(
({ path, eventPoints, openNoteDialog, destCode }) => {
//Определяем нужные параметры
const { source, destination } = path;
//Если путь не указан
if (!destination) {
return;
}
//Если происходит изменение статуса
if (destination.droppableId !== source.droppableId) {
//Конвертим ID переносимого события
let nDraggableTaskId = parseInt(path.draggableId);
//Считываем строку, у которой изменяется статус
let task = tasks.rows.find(r => r.id === nDraggableTaskId);
//Изменяем статус у события
task.statusId = parseInt(path.destination.droppableId);
//Получение настройки точки назначения
const pointSettings = eventPoints.find(eventPoint => eventPoint.SEVPOINT === destCode);
//Изменяем статус события с добавлением примечания
handleTaskStateChange({
nEvent: task.nRn,
sNextStat: destCode,
onReload: () => setTasks(pv => ({ ...pv, reload: true, accountsReload: true })),
onNoteOpen: pointSettings.ADDNOTE_ONCHST ? openNoteDialog : null
});
}
},
[handleTaskStateChange, tasks.rows]
);
//При необходимости перезагрузки данных
useEffect(() => {
//Считывание данных с учетом фильтрации
let getTasks = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_LOAD",
args: {
CFILTERS: {
VALUE: object2Base64XML(convertFilterValuesToArray(filterValues), { arrayNodeName: "filters" }),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
CORDERS: { VALUE: object2Base64XML(ordersValues, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NINCLUDE_ACCOUNTS: tasks.accountsReload ? 1 : 0
},
isArray: name => name === "XAGENTS",
respArg: "COUT"
});
//Считываем информацию о событиях
let events = data.XCLNEVENTS.XDATA.XDATA_GRID;
//Считываем иноформацию о контрагентах
let accounts = tasks.accountsReload ? [...(data.XAGENTS_WITH_IMG.XAGENTS || [])] : tasks.loadedAccounts;
//Инициализируем события
let newRows = [];
//Если есть события
if (events.rows) {
//Формируем структуру событий
newRows = [...(events.rows || [])].reduce(
(prev, cur) => [...prev, initTask(prev.length, cur, accounts.find(agent => agent.SAGNABBR === cur.SSENDER)?.BIMAGE)],
[]
);
}
//Возвращаем информацию
return { rows: [...newRows], loadedAccounts: accounts };
};
//Считывание данных
let getData = async () => {
//Считываем информацию о задачах
let eventTasks = await getTasks();
//Загружаем данные
setTasks(pv => ({
...pv,
loaded: true,
rows: eventTasks.rows,
loadedAccounts: eventTasks.loadedAccounts,
reload: false,
accountsReload: false
}));
};
//Если необходимо загрузить данные и указан тип событий и загружены все необходимые вспомогательные данные
if (tasks.reload) {
//Загружаем данные
getData();
}
}, [SERV_DATA_TYPE_CLOB, executeStored, filterValues, ordersValues, tasks.accountsReload, tasks.loadedAccounts, tasks.reload]);
return [tasks, setTasks, onDragEnd];
};
//----------------
//Интерфейс модуля
//----------------
export { useTasksFunctions, useTasks };

View File

@ -1,16 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Панель мониторинга: Точка входа
*/
//---------------------
//Подключение библиотек
//---------------------
import { ClntTaskBoard } from "./clnt_task_board"; //Корневая панель выполнения работ
//----------------
//Интерфейс модуля
//----------------
export const RootClass = ClntTaskBoard;

View File

@ -1,294 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Дополнительная разметка и вёрстка клиентских элементов
*/
//---------
//Константы
//---------
//Перечисление "Состояние события"
export const EVENT_STATES = Object.freeze({ 0: "Все", 1: "Не аннулированные", 2: "Аннулированные" });
//Допустимые значение поля сортировки
export const sortAttrs = [
{ id: "SEVNSTAT_CODE", descr: "Мнемокод" },
{ id: "SEVNSTAT_NAME", descr: "Наименование" },
{ id: "SEVPOINT_DESCR", descr: "Описание точки маршрута" }
];
//Допустимые значения направления сортировки
export const sortDest = [];
sortDest[-1] = "desc";
sortDest[1] = "asc";
//Цвета статусов
export const COLORS = [
"mediumSlateBlue",
"lightSalmon",
"fireBrick",
"orange",
"gold",
"limeGreen",
"yellowGreen",
"mediumAquaMarine",
"paleTurquoise",
"steelBlue",
"skyBlue",
"tan"
];
//Перечисление "Цвет задачи"
export const TASK_COLORS = Object.freeze({ EXPIRED: "#ff0000", EXPIRES_SOON: "#ffdf00", LINKED: "#1e90ff" });
//Перечисление Доп. свойства "Значение по умолчанию"
export const DP_DEFAULT_VALUE = Object.freeze({ 0: "SDEFAULT_STR", 1: "NDEFAULT_NUM", 2: "DDEFAULT_DATE", 3: "NDEFAULT_NUM" });
//Перечисление Доп. свойства "Префикс формата данных"
export const DP_TYPE_PREFIX = Object.freeze({ 0: "S", 1: "N", 2: "D", 3: "N" });
//Перечисление Доп. свойства "Входящее значение дополнительного словаря"
export const DP_IN_VALUE = Object.freeze({ 0: "pos_str_value", 1: "pos_num_value", 2: "pos_date_value", 3: "pos_num_value" });
//Перечисление Доп. свойства "Исходящее значение дополнительного словаря"
export const DP_RETURN_VALUE = Object.freeze({ 0: "str_value", 1: "num_value", 2: "date_value", 3: "num_value" });
//-----------
//Тело модуля
//-----------
//Формирование массива из 0, 1 и более элементов
export const makeArray = arr => {
return arr ? (arr.length ? arr : [arr]) : [];
};
//Конвертация формата HEX в формат RGB
const convertHexToRGB = hex => {
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
let a = 0.5;
r = Math.round((a * (r / 255) + a * (255 / 255)) * 255);
g = Math.round((a * (g / 255) + a * (255 / 255)) * 255);
b = Math.round((a * (b / 255) + a * (255 / 255)) * 255);
return "rgb(" + r + ", " + g + ", " + b + ")";
};
//Считывание заливки события по условию
export const getTaskBgColorByRule = (task, colorRule) => {
//Инициализируем значения
let ruleCode = "";
//Исходя из типа определяем наименование
switch (colorRule.STYPE) {
case "number":
ruleCode = `N${colorRule.SFIELD}`;
break;
case "date":
ruleCode = `D${colorRule.SFIELD}`;
break;
default:
ruleCode = `S${colorRule.SFIELD}`;
break;
}
//Определяем цвет заливки
let bgColor = ruleCode && task.docProps[ruleCode] == colorRule.fromValue ? convertHexToRGB(colorRule.SCOLOR) : null;
//Возвращаем цвет заливки
return bgColor;
};
//Индикация истечения срока отработки события
export const getTaskExpiredColor = task => {
//Определяем текущую дату
let sysDate = new Date();
//Определяем дату истечения срока события
let expireDate = task.dExpireDate ? new Date(task.dExpireDate) : null;
//Если дата истечения срока определена
if (expireDate) {
//Определяем разницу между датами
let daysDiff = ((expireDate.getTime() - sysDate.getTime()) / (1000 * 60 * 60 * 24)).toFixed(2);
//Если разница меньше 0 - срок истечен
if (daysDiff < 0) return TASK_COLORS.EXPIRED;
//Если разница меньше 4 - скоро истечет
if (daysDiff < 4) return TASK_COLORS.EXPIRES_SOON;
}
return null;
};
//Цвет из hsl формата в rgba формат
const convertHslToRgba = (h, s, l) => {
s /= 100;
l /= 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return `rgba(${Math.floor(255 * f(0))},${Math.floor(255 * f(8))},${Math.floor(255 * f(4))},0.3)`;
};
//Формирование случайного цвета
export const getRandomColor = index => {
const hue = index * 137.508;
return convertHslToRgba(hue, 50, 70);
};
//Формат дополнительного свойства типа число (длина, точность)
const formatRegExpNum = (width, precision) =>
new RegExp("^(\\d{1," + (width - precision) + "}" + (precision > 0 ? "((\\.|,)\\d{1," + precision + "})?" : "") + ")?$");
//Формат дополнительного свойства типа строка (длина)
const formatRegExpStr = length => new RegExp("^.{0," + length + "}$");
//Проверка валидности числа
const isValidNum = (width, precision, value) => {
return formatRegExpNum(width, precision).test(value);
};
//Проверка валидности строки
const isValidStr = (length, value) => {
return formatRegExpStr(length).test(value);
};
//Признак ошибки валидации
export const validationError = (value = "", format, numWidth, numPrecision, strLength) => {
//Исходим от формата
switch (format) {
//Проверка строки
case 0:
return isValidStr(strLength, value);
//Проверка числа
case 1:
return isValidNum(numWidth, numPrecision, value);
//Остальное не проверяем
default:
return true;
}
};
//Конвертация времени в привычный формат
export const formatSqlDate = timeStamp => {
//Если есть разделитель
if (timeStamp.indexOf(".") !== -1) {
//Определяем секунды
let seconds = 24 * 60 * 60 * timeStamp;
//Определяем часы
const hours = Math.trunc(seconds / (60 * 60));
//Переопределяем секунды
seconds = seconds % (60 * 60);
//Определяем минуты
const minutes = Math.trunc(seconds / 60);
//Определяем остаток секунд
seconds = Math.round(seconds % 60);
//Форматируем
const formattedTime = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2) + ":" + ("0" + seconds).slice(-2);
//Возвращаем результат
return formattedTime;
}
return timeStamp;
};
//Считывание значений из локального хранилища
export const getLocalStorageValue = (sName, defaultValue = null) => {
return localStorage.getItem(sName) ? localStorage.getItem(sName) : defaultValue;
};
//Форматирование фильтра в массив для отбора
export const convertFilterValuesToArray = filterValues => {
//Инициализируем значение "с" состояния ("Все", "Не аннулированные" - 0, "Аннулированые" - 1)
let nClosedFrom = filterValues.sState ? ([EVENT_STATES[0], EVENT_STATES[1]].includes(filterValues.sState) ? 0 : 1) : 0;
//Инициализируем значение "по" состояния ("Все", "Аннулированные" - 1, "Не аннулированные" - 0)
let nClosedTo = filterValues.sState ? ([EVENT_STATES[0], EVENT_STATES[2]].includes(filterValues.sState) ? 1 : 0) : 0;
//Формируем массив значений фильтра
let filterValuesArray = [
{ name: "NCLOSED", from: nClosedFrom, to: nClosedTo },
{ name: "SEVTYPE_CODE", from: filterValues.sType, to: null },
{ name: "NCRN", from: filterValues.sCrnRnList, to: null },
{ name: "SSEND_PERSON", from: filterValues.sSendPerson, to: null },
{ name: "SSEND_DIVISION", from: filterValues.sSendDivision, to: null },
{ name: "SSEND_USRGRP", from: filterValues.sSendUsrGrp, to: null },
{ name: "NLINKED_RN", from: filterValues.sDocLink, to: null }
];
return filterValuesArray;
};
//Формирование массива действий карточки события
export const makeCardActionsArray = (onEdit, onEditClient, onDelete, onStateChange, onReturn, onSend, onNotesOpen, onFileLinksOpen) => {
//Формируем список действий карточки
return [
{
method: "EDIT",
name: "Исправить",
icon: "edit",
visible: false,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onEdit
},
{
method: "EDIT_CLIENT",
name: "Исправить в разделе",
icon: "edit_note",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onEditClient
},
{
method: "DELETE",
name: "Удалить",
icon: "delete",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: false,
func: onDelete
},
{
method: "TASK_STATE_CHANGE",
name: "Перейти",
icon: "turn_right",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
func: onStateChange
},
{
method: "TASK_RETURN",
name: "Выполнить возврат",
icon: "turn_left",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
func: onReturn
},
{
method: "TASK_SEND",
name: "Направить",
icon: "send",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: true,
func: onSend
},
{
method: "NOTES",
name: "Примечания",
icon: "event_note",
visible: true,
delimiter: true,
tasksReload: false,
needAccountsReload: false,
func: onNotesOpen
},
{
method: "FILE_LINKS",
name: "Присоединенные документы",
icon: "attach_file",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onFileLinksOpen
}
];
};

View File

@ -1,48 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Общие стили
*/
//---------------------
//Подключение библиотек
//---------------------
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Общие стили
export const COMMON_STYLES = {
TASK_FORM_TEXT_FIELD: (widthVal, greyDisabled = false) => ({
margin: "4px",
...(widthVal ? { width: widthVal } : {}),
...(greyDisabled
? {
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.87)"
},
"& .MuiInputLabel-root.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.6)"
}
}
: {})
}),
BOX_WITH_LEGEND: { border: "1px solid #939393" },
BOX_SINGLE_COLUMN: { display: "flex", flexDirection: "column", gap: "10px" },
LEGEND: { textAlign: "left" },
SELECT_MENU: width => {
return { overflowY: "auto", ...APP_STYLES.SCROLL, width: width ? width : null };
},
STACK_DOCLINKS: { alignItems: "baseline" },
SCROLL: { ...APP_STYLES.SCROLL, overflowY: "auto" },
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
DIALOG_CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
ZERO_PADDING: { padding: 0 }
};

View File

@ -1,180 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Диалог формы события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useCallback, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogContent, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
import { useClientEvent } from "./hooks/task_dialog_hooks"; //Хук для события
import { TaskForm } from "./components/task_form"; //Форма события
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../core/utils"; //Вспомогательные функции
import { COMMON_STYLES } from "./styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
DIALOG_CONTENT: {
paddingBottom: "0px",
maxHeight: "740px",
minHeight: "740px",
...COMMON_STYLES.SCROLL
}
};
//-----------
//Тело модуля
//-----------
//Диалог формы события
const TaskDialog = ({ taskRn, taskType, taskStatus, editable, onTasksReload, onClose }) => {
//Собственное состояние
const [task, setTask] = useClientEvent(taskRn, taskType, taskStatus);
//Состояние заполненности всех обязательных свойств
const [dpReady, setDPReady] = useState(false);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При изменении заполненности всех обязательных свойств
const handleDPReady = useCallback(v => setDPReady(v), []);
//При изменении информации о задаче
const handleTaskChange = useCallback(
newTaskValues => {
setTask(pv => ({ ...pv, ...newTaskValues }));
},
[setTask]
);
//При добавлении события
const handleInsertTask = async callBack => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INSERT",
args: {
SCRN: task.sCrn,
SPREF: task.sPrefix,
SNUMB: task.sNumber,
STYPE: task.sType,
SSTATUS: task.sStatus,
SPLAN_DATE: task.dPlanDate,
SINIT_PERSON: task.sInitClnperson,
SCLIENT_CLIENT: task.sClntClients,
SCLIENT_PERSON: task.sClntClnperson,
SDESCRIPTION: task.sDescription,
SREASON: task.sInitReason,
CPROPS: {
VALUE: object2Base64XML(
[
Object.fromEntries(
Object.entries(task.docProps)
// eslint-disable-next-line no-unused-vars
.filter(([_, v]) => v != (null || ""))
)
],
{
arrayNodeName: "props"
}
),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
}
});
callBack();
};
//При исправлении события
const handleUpdateEvent = async callBack => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_UPDATE",
args: {
NCLNEVENTS: task.nRn,
SCLIENT_CLIENT: task.sClntClients,
SCLIENT_PERSON: task.sClntClnperson,
SDESCRIPTION: task.sDescription,
CPROPS: {
// eslint-disable-next-line no-unused-vars
VALUE: object2Base64XML([Object.fromEntries(Object.entries(task.docProps).filter(([_, v]) => v != (null || "")))], {
arrayNodeName: "props"
}),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
}
});
callBack();
};
//При считывании следующего номера события
const handleEventNextNumbGet = useCallback(async () => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_NEXTNUMB_GET",
args: {
SPREFIX: task.sPrefix
}
});
//Если данные есть
if (data) {
//Устанавливаем номер
setTask(pv => ({ ...pv, sNumber: data.SEVENT_NUMB }));
}
}, [executeStored, setTask, task.sPrefix]);
//Генерация содержимого
return (
<Dialog open onClose={onClose ? onClose : null} fullWidth>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<TaskForm
task={task}
taskType={taskType}
onTaskChange={handleTaskChange}
editable={!taskRn || editable ? true : false}
onEventNextNumbGet={handleEventNextNumbGet}
onDPReady={handleDPReady}
/>
</DialogContent>
{onClose ? (
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
{taskRn ? (
<Button
onClick={() => handleUpdateEvent(onClose).then(onTasksReload)}
disabled={task.updateDisabled || !editable || !dpReady}
>
Исправить
</Button>
) : (
<Button onClick={() => handleInsertTask(onClose).then(onTasksReload)} disabled={task.insertDisabled || !dpReady}>
Добавить
</Button>
)}
<Button onClick={onClose}>Закрыть</Button>
</DialogActions>
) : null}
</Dialog>
);
};
//Контроль свойств - Диалог формы события
TaskDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
taskStatus: PropTypes.string,
editable: PropTypes.bool,
onTasksReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskDialog };

View File

@ -19,11 +19,12 @@ import { DATA_TYPE } from "../../common"; //Общие ресурсы и кон
//Типовые цвета точек привязки
const HANDLE_BORDER_COLOR = "#69db7c";
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
//Стили
const STYLES = {
CONTAINER: { display: "flex", width: "100%", height: "100%" },
CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
HANDLE_SOURCE: isConnecting => ({
width: 14,
height: 14,
@ -32,17 +33,18 @@ const STYLES = {
borderRadius: 7,
background: "white"
}),
HANDLE_TARGET: isConnecting => ({
HANDLE_TARGET: (isConnecting, isValidConnection) => ({
width: isConnecting ? 14 : 0,
height: 14,
left: isConnecting ? -7 : 0,
border: `2px solid ${HANDLE_BORDER_COLOR}`,
border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
borderRadius: 7,
background: "white",
visibility: isConnecting ? "visible" : "hidden"
}),
CONTENT_STACK: { width: "100%" },
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" }
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" },
ATTR_PROP_ICON: { fontSize: "0.9rem" }
};
//Иконки
@ -55,11 +57,36 @@ const ICONS = {
//Структура данных об атрибуте сущности
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
parentEntity: PropTypes.string,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
dataType: PropTypes.number.isRequired
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
agg: PropTypes.string,
alias: PropTypes.string,
use: PropTypes.oneOf([0, 1]).isRequired,
show: PropTypes.oneOf([0, 1]).isRequired
});
//-----------------------
//Вспомогательные функции
//-----------------------
//Получение атрибутики состояния включения атрибута в запрос
const attrGetUse = (attr, callToAction = false) => {
return [attr.use === 1, `${attr.use === 1 ? "Включен в запрос" : "Не включен в запрос"}${callToAction ? "- нажмите, чтобы изменить" : ""}`];
};
//Получение атрибутики состояния отображения атрибута в результатах запроса
const attrGetShow = (attr, callToAction = false) => {
return [
`${attr.show == 1 ? "Отображается в результатах запроса" : "Не отображается в результатах запроса"}${
callToAction ? "- нажмите, чтобы изменить" : ""
}`,
attr.show == 1 ? "visibility" : "visibility_off"
];
};
//-----------
//Тело модуля
//-----------
@ -67,25 +94,45 @@ const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
//Атрибут сущности
const Attribute = ({ data }) => {
//Поиск идентификатора соединяемого элемента
const connectionNodeId = useStore(state => state.connectionNodeId);
const [connectionNodeId, targetConnectionNode, connectionStatus] = useStore(state => [
state.connectionNodeId,
state?.connectionEndHandle?.nodeId,
state.connectionStatus
]);
//Флаг выполнения соединения сущностей
const isConnecting = Boolean(connectionNodeId);
//Флаг корректности соединения сущностей
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
//Получим атрибуты состояния отображения
const [showTitle, showIcon] = attrGetShow(data);
//Формирование представления
return (
<Box p={1} sx={STYLES.CONTAINER}>
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
<Handle type={"target"} position={Position.Left} isConnectableStart={false} style={STYLES.HANDLE_TARGET(isConnecting)} />
<Handle
type={"target"}
position={Position.Left}
isConnectableStart={false}
style={STYLES.HANDLE_TARGET(isConnecting, isValidConnection)}
/>
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
<Typography variant={"body2"} noWrap title={data.title}>
{data.title}
</Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
{data.name}
</Typography>
<Stack direction={"row"} alignItems={"center"} spacing={0.5}>
<Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
{`${data.name},`}
</Typography>
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
{showIcon}
</Icon>
</Stack>
</Stack>
</Stack>
</Box>
@ -101,4 +148,4 @@ Attribute.propTypes = {
//Интерфейс модуля
//----------------
export { Attribute };
export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow };

View File

@ -17,6 +17,7 @@ import "./entity.css"; //Стили компомнента
//Структура данных о сущности запроса
const ENTITY_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired
});
@ -26,7 +27,7 @@ const ENTITY_DATA_SHAPE = PropTypes.shape({
//-----------
//Сущность запроса
const Entity = ({ data, selected }) => {
const Entity = ({ data, selected = false }) => {
return (
<div className="entity__wrapper" data-selected={selected}>
<div className="entity__title">
@ -47,4 +48,4 @@ Entity.propTypes = {
//Интерфейс модуля
//----------------
export { Entity };
export { Entity, ENTITY_DATA_SHAPE };

View File

@ -0,0 +1,97 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Список атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, ListItemIcon, Checkbox, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow } from "../attribute/attribute"; //Атрибут сущности
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
//---------
//Константы
//---------
//Стили
const STYLES = {
SMALL_TOOL_ICON: {
fontSize: 20
},
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список атрибутов сущности
const AttrsList = ({ attrs = [], filter, onSelect = null, onShow = null } = {}) => {
//При выборе элемента списка
const handleSelectClick = attr => {
onSelect && onSelect(attr);
};
//При нажатии на исправлении
const handleShowClick = (e, attr) => {
e.stopPropagation();
onShow && onShow(attr);
};
//Рег. выражение для фильтра
const filterRegExp = filter ? new RegExp(filter, "i") : null;
//Формирование представления
return (
<List sx={STYLES.LIST}>
{attrs &&
attrs
.filter(attr => (filterRegExp ? filterRegExp.test(attr.name) || filterRegExp.test(attr.title) : true))
.map((attr, i) => {
const [selected, selectedTitle] = attrGetUse(attr, true);
const [showTitle, showIcon] = attrGetShow(attr, true);
return (
<ListItem key={i} disablePadding>
<ListItemButton onClick={() => handleSelectClick(attr)} selected={selected} dense>
<ListItemIcon>
<Checkbox edge="start" checked={selected} tabIndex={-1} disableRipple title={selectedTitle} />
</ListItemIcon>
<ListItemText
primary={attr.title}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{`${attr.alias || attr.name}`}</Typography>
</Stack>
}
/>
<Stack direction={"row"}>
<IconButton onClick={e => handleShowClick(e, attr)} title={showTitle}>
<Icon>{showIcon}</Icon>
</IconButton>
</Stack>
</ListItemButton>
</ListItem>
);
})}
</List>
);
};
//Контроль свойств компонента - Список атрибутов сущности
AttrsList.propTypes = {
attrs: PropTypes.arrayOf(ATTRIBUTE_DATA_SHAPE),
filter: PropTypes.string,
onSelect: PropTypes.func,
onShow: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { AttrsList };

View File

@ -0,0 +1,111 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог настройки атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, InputAdornment, Icon, IconButton } from "@mui/material"; //Интерфейсные элементы MUI
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { AttrsList } from "./attrs_list"; //Список атрибутов сущности
import { useEntityAttrs } from "./hooks"; //Хуки диалога настройки атрибутов сущности
//-----------
//Тело модуля
//-----------
//Диалог настройки атрибутов сущности
const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => {
//Собственное состояние - фильтр атрибутов
const [filter, setFilter] = useState("");
//Собственное состояние - список атрибутов
const [attrs, setAttrs] = useState([]);
//Хук для взаимодействия с сервером
const [srvAttrs, saveAttrs] = useEntityAttrs(query, id);
//Нажатие на кнопку "Ok"
const handleOk = async () => {
await saveAttrs(attrs);
onOk && onOk();
};
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//Выбор/исключение атрибута из запроса
const handleAttrSelect = attr =>
setAttrs(
attrs.map(a => ({
...(a.id === attr.id ? { ...a, use: a.use === 1 ? 0 : 1, show: a.use === 1 ? 0 : a.show } : a)
}))
);
//Отображение/сокрытие атрибута в запросе
const handleAttrShow = attr =>
setAttrs(
attrs.map(a => ({
...(a.id === attr.id ? { ...a, show: a.show === 1 ? 0 : 1, use: a.show === 0 ? 1 : a.use } : a)
}))
);
//При изменении значения фильтра
const handleFilterChange = e => setFilter(e.target.value);
//При очистке фильтра
const handleFilterClear = () => setFilter("");
//При загрузке данных с сервера
useEffect(() => {
if (srvAttrs) setAttrs(srvAttrs.map(srvAttr => ({ ...srvAttr })));
}, [srvAttrs]);
//Генерация содержимого
return (
<P8PDialog title={`Атрибуты сущности "${title}"`} onOk={handleOk} onCancel={handleCancel}>
<TextField
margin={"normal"}
variant={"standard"}
fullWidth
placeholder={"Поиск атрибута..."}
value={filter}
onChange={handleFilterChange}
InputProps={{
startAdornment: (
<InputAdornment position={"start"}>
<Icon>search</Icon>
</InputAdornment>
),
endAdornment: (
<InputAdornment position={"end"}>
<IconButton onClick={handleFilterClear}>
<Icon>clear</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
<AttrsList attrs={attrs} filter={filter} onSelect={handleAttrSelect} onShow={handleAttrShow} />
</P8PDialog>
);
};
//Контроль свойств - Диалог настройки атрибутов сущности
EntityAttrsDialog.propTypes = {
query: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntityAttrsDialog };

View File

@ -0,0 +1,92 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки диалога настройки атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../../core/utils"; //Вспомогательные функции
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//-----------
//Тело модуля
//-----------
//Работа с атрибутами сущности
const useEntityAttrs = (query, entity) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Обновление данных
const doRefresh = () => setRefresh(true);
//Установка атрибутов сущности
const setAttrs = useCallback(
async attrs => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_SET",
args: {
NRN: query,
SID: entity,
CATTRS: {
VALUE: object2Base64XML(attrs, { arrayNodeName: "XATTR", ignoreAttributes: false, attributeNamePrefix: "" }),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
},
loader: false
});
},
[query, entity, executeStored, SERV_DATA_TYPE_CLOB]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_GET",
args: { NRN: query, SID: entity },
respArg: "COUT",
isArray: name => ["XATTR"].includes(name),
attributeValueProcessor: (name, val) => (["name", "title", "agg"].includes(name) ? undefined : val),
loader: true
});
setData(data?.XATTRS?.XATTR || []);
} finally {
setRefresh(false);
setLoading(false);
}
};
//Если надо обновить
if (refresh)
//Получим данные
loadData();
}, [refresh, query, entity, executeStored]);
//Возвращаем интерфейс хука
return [data, setAttrs, doRefresh, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useEntityAttrs };

View File

@ -10,6 +10,7 @@
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
//---------
@ -20,14 +21,15 @@ import { BUTTONS } from "../../../../../app.text"; //Общие текстовы
const STYLES = {
SMALL_TOOL_ICON: {
fontSize: 20
}
},
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//---------
//Константы
//---------
//Структура данных о сущности запроса
//Структура элемента списка запросов
const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
rn: PropTypes.number.isRequired,
code: PropTypes.string.isRequired,
@ -43,7 +45,7 @@ const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
//Тело модуля
//-----------
//Диалог открытия запроса
//Список запросов
const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = null, onReady = null, onEdit = null, onDelete = null } = {}) => {
//При выборе элемента списка
const handleSelectClick = query => {
@ -76,7 +78,7 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
//Формирование представления
return (
<List sx={{ height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" }}>
<List sx={STYLES.LIST}>
{queries.map((query, i) => {
const selected = query.rn === current;
const disabled = !query.modify;
@ -85,8 +87,8 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch";
return (
<ListItem key={i}>
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected}>
<ListItem key={i} disablePadding>
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected} dense>
<ListItemText
primary={query.name}
secondaryTypographyProps={{ component: "div" }}

View File

@ -36,7 +36,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null
const handleQueryAdd = () => setModQuery(true);
//При выборе запроса
const handleQuerySelect = query => onQuerySelect && onQuerySelect(query.rn);
const handleQuerySelect = query => onQuerySelect && onQuerySelect({ ...query });
//При установке признака публичности
const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1);

View File

@ -8,10 +8,11 @@
//---------------------
import React, { useState, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
import { Entity } from "../entity/entity"; //Сущность запроса
import { Attribute } from "../attribute/attribute"; //Атрибут сущности
import { Entity, ENTITY_DATA_SHAPE } from "../entity/entity"; //Сущность запроса
import { Attribute, ATTRIBUTE_DATA_SHAPE } from "../attribute/attribute"; //Атрибут сущности
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
import "./query_diagram.css"; //Стили компонента
@ -36,42 +37,48 @@ const NODE_TYPES_COMPONENTS = {
[NODE_TYPE.ATTRIBUTE]: Attribute
};
//Структура сущности запроса
const ENTITY_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.oneOf([NODE_TYPE.ENTITY, NODE_TYPE.ATTRIBUTE]).isRequired,
style: PropTypes.object,
position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }),
draggable: PropTypes.bool.isRequired,
data: PropTypes.oneOfType([ENTITY_DATA_SHAPE, ATTRIBUTE_DATA_SHAPE])
});
//Структура связи запроса
const RELATION_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired });
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Проверка зацикленности связи
const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
if (visited.has(target.id)) {
return false;
}
if (visited.has(target.id)) return false;
visited.add(target.id);
for (const outgoer of getOutgoers(target, nodes, edges)) {
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) {
return true;
}
}
for (const outgoer of getOutgoers(target, nodes, edges))
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true;
return false;
};
//Проверка корректности связи
const isValidConnection = (connection, nodes, edges) => {
if (!connection.source || !connection.target) {
return false;
}
const tableId = connection.source.split("-")[0];
const isSameTable = connection.target.startsWith(tableId);
if (isSameTable) {
return false;
}
//Должны быть заданы источник и приёмник
if (!connection.source || !connection.target) return false;
//Найдем источник и приёмник
const source = nodes.find(node => node.id === connection.source);
const target = nodes.find(node => node.id === connection.target);
if (!target || target.id === connection.source) {
return false;
}
//Приёмник и источник должны существовать
if (!target || !source) return false;
//Нельзя ссылаться на самого себя
if (source?.data?.parentEntity == target?.data?.parentEntity) return false;
//Типы данны источника и приёмника должны совпадать
if (source?.data?.dataType != target?.data?.dataType) return false;
//Приёмник должен не должен быть источником
if (target.id === connection.source) return false;
//Нельзя зацикливаться
return !hasCycle(connection, target, nodes, edges);
};
@ -80,7 +87,18 @@ const isValidConnection = (connection, nodes, edges) => {
//-----------
//Диаграмма запроса
const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRemove, onRelationAdd, onRelationRemove }) => {
const QueryDiagram = ({
entities = [],
relations = [],
onEntityClick,
onEntityAttrClick,
onEntityPositionChange,
onEntityRemove,
onRelactionClick,
onRelationAdd,
onRelationRemove,
onPaneClick
}) => {
//Собственное состояние - элементы
const [nodes, setNodes] = useState(entities);
@ -93,17 +111,44 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
//При изменении элементов на диаграмме
const handleNodesChange = useCallback(
changes => {
setNodes(nodesSnapshot => applyNodeChanges(changes, nodesSnapshot));
//При выборе атрибута подсветим всю сущность
const tmpChanges = changes.reduce((prevChanges, curChanges) => {
const tmp = { ...curChanges };
if (tmp.type == "select") {
const chEnt = entities.find(e => e.id === tmp.id);
if (chEnt && chEnt?.data?.parentEntity) {
prevChanges.push({ ...curChanges, id: chEnt.data.parentEntity });
tmp.selected = false;
}
}
prevChanges.push(tmp);
return prevChanges;
}, []);
//Применим изменения в диаграмме
setNodes(nodesSnapshot => applyNodeChanges(tmpChanges, nodesSnapshot));
//Если двигали сущность - запомним начало движения
if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
//Если закончили двигать сущность и ранее запомнили начало движения - вызываем колбэки для нажатия и двиения сущности
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
if (onEntityClick) onEntityClick(movedNode.id);
setMovedNode(null);
}
//Если удалили сущность - вызываем колбэк для удаления
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
onEntityRemove(changes[0].id);
},
[movedNode, entities, onEntityPositionChange, onEntityRemove]
[movedNode, entities, onEntityClick, onEntityPositionChange, onEntityRemove]
);
//При выборе элемента диаграммы
const handleNodeClick = useCallback(
(e, node) =>
node?.type == NODE_TYPE.ENTITY
? onEntityClick && onEntityClick(node?.id)
: onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
[onEntityClick, onEntityAttrClick]
);
//При связывании элементов на диаграмме
@ -112,6 +157,9 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
onRelationAdd && onRelationAdd(connection.source, connection.target);
};
//При выборе связи диаграммы
const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);
//При изменении связей на диаграмме
const handleEdgesChange = useCallback(
changes => {
@ -121,9 +169,13 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
[onRelationRemove]
);
const validateConnection = connection => {
return isValidConnection(connection, nodes, edges);
};
//При нажатии на холст диаграммы
const handlePaneClick = () => onPaneClick && onPaneClick();
//Валидация связи
const validateConnection = connection => isValidConnection(connection, nodes, edges);
//Подсветка выбранной сущности
//При изменении состава сущностей
useEffect(() => setNodes(entities), [entities]);
@ -137,8 +189,11 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
nodes={nodes}
nodeTypes={NODE_TYPES_COMPONENTS}
edges={edges}
onNodeClick={handleNodeClick}
onNodesChange={handleNodesChange}
onEdgeClick={handleEdgeClick}
onEdgesChange={handleEdgesChange}
onPaneClick={handlePaneClick}
defaultEdgeOptions={{
animated: true,
style: STYLES.EDGE
@ -153,6 +208,20 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
);
};
//Контроль свойств компонента - Диаграмма запроса
QueryDiagram.propTypes = {
entities: PropTypes.arrayOf(ENTITY_SHAPE),
relations: PropTypes.arrayOf(RELATION_SHAPE),
onEntityClick: PropTypes.func,
onEntityAttrClick: PropTypes.func,
onEntityPositionChange: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelactionClick: PropTypes.func,
onRelationAdd: PropTypes.func,
onRelationRemove: PropTypes.func,
onPaneClick: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------

View File

@ -0,0 +1,122 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Свойства запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { EntityAddDialog } from "../entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности
import { EntityAttrsDialog } from "../entity_attrs_dialog/entity_attrs_dialog"; //Диалог настройки атрибутов сущности
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { ENTITY_DATA_SHAPE } from "../entity/entity"; //Описание сущности
import { RELATION_DATA_SHAPE } from "../relation/relation"; //Описание связи
//-----------
//Тело модуля
//-----------
//Свойства запроса
const QueryOptions = ({ query, entity, relation, onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged }) => {
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
//Отображение диалога настройки атрибутов сущности
const [openEntityAttrsDialog, setOpenEntityAttrsDialog] = useState(false);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//При нажатии на кнопку добавлении сущности в запрос
const handleEntityAddClick = () => setOpenEntityAddDialog(true);
//При нажатии на кнопку настройки атрибутов сущности
const handleEntityAttrsClick = () => setOpenEntityAttrsDialog(true);
//При нажатии на кнопку даления сущности из запроса
const handleEntityRemoveClick = () =>
showMsgWarn(`Удалить сущность "${entity.title}"?`, () => entity?.id && onEntityRemove && onEntityRemove(entity.id));
//При нажатии на кнопку даления связи из запроса
const handleRelationRemoveClick = () =>
showMsgWarn(
`Удалить связь "${relation.source}" - "${relation.target}"?`,
() => relation?.id && onRelationRemove && onRelationRemove(relation.id)
);
//Закрытие диалога добавления сущности по "Отмена"
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
//Закрытие диалога добавления сущности по "ОК"
const handleEntityAddDialogOk = values => onEntityAdd && onEntityAdd(values.name, res => res && setOpenEntityAddDialog(false));
//Закрытие диалога настройки атрибутов сущности по "Отмена"
const handleEntityAttrsDialogCancel = () => setOpenEntityAttrsDialog(false);
//Закрытие диалога настройки атрибутов сущности по "ОК"
const handleEntityAttrsDialogOk = () => {
onQueryOptionsChanged && onQueryOptionsChanged();
setOpenEntityAttrsDialog();
};
//Генерация содержимого
return (
<>
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
{openEntityAttrsDialog && (
<EntityAttrsDialog query={query} {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />
)}
<P8PEditorBox title={"Настройки запроса"}>
<P8PEditorSubHeader title={"Параметры"} />
<P8PEditorSubHeader title={"Условия отбора"} />
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
{BUTTONS.INSERT}
</Button>
{entity && (
<>
<P8PEditorSubHeader title={entity.title} />
<Button startIcon={<Icon>edit_attributes</Icon>} onClick={handleEntityAttrsClick}>
Атрибуты
</Button>
<Button startIcon={<Icon>delete</Icon>} onClick={handleEntityRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
{relation && (
<>
<P8PEditorSubHeader title={"Связь"} />
<Button startIcon={<Icon>delete</Icon>} onClick={handleRelationRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
</P8PEditorBox>
</>
);
};
//Контроль свойств компонента - Свойства запроса
QueryOptions.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_DATA_SHAPE,
relation: RELATION_DATA_SHAPE,
onEntityAdd: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelationRemove: PropTypes.func,
onQueryOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { QueryOptions };

View File

@ -0,0 +1,27 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Связь сущностей запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//---------
//Константы
//---------
//Структура данных о связи сущностей запроса
const RELATION_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
source: PropTypes.string.isRequired,
target: PropTypes.string.isRequired
});
//----------------
//Интерфейс модуля
//----------------
export { RELATION_DATA_SHAPE };

View File

@ -7,22 +7,23 @@
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import { Box, Grid, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
import React, { useState, useContext } from "react"; //Классы React
import { Box, Grid } from "@mui/material"; //Интерфейсные компоненты MUI
import { ApplicationСtx } from "../../context/application"; //Контекст взаимодействия с приложением
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы приложения
import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса
import { QueryOptions } from "./components/query_options/query_options"; //Свойства запроса
import { QueriesManager } from "./components/queries_manager/queries_manager"; //Менеджер запросов
import { EntityAddDialog } from "./components/entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности
import { P8PEditorBox } from "../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
import { P8PEditorSubHeader } from "../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { useQueryDesc } from "./hooks"; //Пользовательские хуки
//---------
//Константы
//---------
//Заголовок панели по умолчанию
const APP_BAR_TITLE_DEFAULT = "Редактор запросов";
//Стили
const STYLES = {
CONTAINER: { display: "flex" },
@ -39,26 +40,79 @@ const QueryEditor = () => {
//Текущий запрос
const [query, setQuery] = useState(null);
//Текущая сущность
const [entity, setEntity] = useState(null);
//Текущая связь
const [relation, setRelation] = useState(null);
//Отображения менеджера запросов
const [openQueriesManager, setOpenQueriesManager] = useState(true);
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
//Получение данных запроса
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl] = useQueryDesc(query);
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh] = useQueryDesc(query);
//Подключение к контексту приложения
const { setAppBarTitle } = useContext(ApplicationСtx);
//Выбор сущности
const selectEntity = ent => {
setRelation(null);
const queryEnt = queryDiagram.entities.find(e => e.id === ent);
if (queryEnt) setEntity({ ...queryEnt.data });
};
//Выбор связи
const selectRelation = rl => {
setEntity(null);
const queryRl = queryDiagram.relations.find(r => r.id === rl);
if (queryRl) setRelation({ ...queryRl });
};
//Сброс выбора связи и сущности
const cleanupEnRlSelection = () => {
setRelation(null);
setEntity(null);
};
//Обработка изменения положения сущности на диаграмме
const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y);
//Обработка добавления сущности в запрос
const handleEntityAdd = async (entName, cb) => {
await addEnt(entName, "VIEW");
cb(true);
};
//Обработка удаления сущности из запроса
const handleEntityRemove = ent => removeEnt(ent);
const handleEntityRemove = async ent => {
await removeEnt(ent);
if (entity && entity?.id === ent) cleanupEnRlSelection();
};
//Обработка выделения сущности
const handleEntityClick = ent => selectEntity(ent);
//Обработка выделения тарибута сущности
const handleEntityAttrClick = ent => selectEntity(ent);
//Обработка выделения связи
const handleRelationClick = rl => selectRelation(rl);
//Обработка добавления отношения cущностей
const handleRelationAdd = (source, target) => addRl(source, target);
const handleRelationAdd = (source, target) => {
cleanupEnRlSelection();
addRl(source, target);
};
//Обработка удаления отношения cущностей
const handleRelationRemove = rl => removeRl(rl);
const handleRelationRemove = async rl => {
await removeRl(rl);
if (relation && relation?.id === rl) cleanupEnRlSelection();
};
//При нажатии на панели (пустом месте) диаграммы запроса
const handlePaneClick = () => cleanupEnRlSelection();
//Открытие менеджера запросов
const handleOpenQueriesManager = () => setOpenQueriesManager(true);
@ -67,26 +121,25 @@ const QueryEditor = () => {
const handleCancelQueriesManager = () => setOpenQueriesManager(false);
//Закрытие запроса
const handleQueryClose = () => setQuery(null);
const handleQueryClose = () => {
setAppBarTitle(APP_BAR_TITLE_DEFAULT);
cleanupEnRlSelection();
setQuery(null);
};
//При выборе запроса
const handleQuerySelect = query => {
setQuery(query);
const handleQuerySelect = ({ rn, name }) => {
setAppBarTitle(`Запрос [${name}]`);
setQuery(rn);
setOpenQueriesManager(false);
};
//При добавлении сущности в запрос
const handleEntityAdd = () => setOpenEntityAddDialog(true);
//Закрытие диалога добавления сущности по "ОК"
const handleEntityAddDialogOk = async values => {
await addEnt(values.name, "VIEW");
setOpenEntityAddDialog(false);
//При изменении свойств запроса
const handleQueryOptionsChanged = () => {
cleanupEnRlSelection();
doRefresh();
};
//Закрытие диалога добавления сущности по "ОК"
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
//Панель инструмментов
const toolBar = (
<P8PEditorToolBar
@ -101,28 +154,34 @@ const QueryEditor = () => {
return (
<Box sx={STYLES.CONTAINER}>
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
<Grid item xs={20}>
{queryDiagram && (
<QueryDiagram
{...queryDiagram}
onEntityClick={handleEntityClick}
onEntityAttrClick={handleEntityAttrClick}
onEntityPositionChange={handleEntityPositionChange}
onEntityRemove={handleEntityRemove}
onRelactionClick={handleRelationClick}
onRelationAdd={handleRelationAdd}
onRelationRemove={handleRelationRemove}
onPaneClick={handlePaneClick}
/>
)}
</Grid>
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
{toolBar}
{query && (
<P8PEditorBox title={"Параметры запроса"}>
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAdd}>
{BUTTONS.INSERT}
</Button>
</P8PEditorBox>
<QueryOptions
query={query}
entity={entity}
relation={relation}
onEntityAdd={handleEntityAdd}
onEntityRemove={handleEntityRemove}
onRelationRemove={handleRelationRemove}
onQueryOptionsChanged={handleQueryOptionsChanged}
/>
)}
</Grid>
</Grid>

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,22 @@ create or replace package PKG_P8PANELS_QE as
NY in number -- Координата по оси ординат
);
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
);
/* Установка состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
CATTRS in clob -- Сериализованное описание атрибутов сущности (BASE64 XML)
);
/* Добавление связи в запрос */
procedure QUERY_RL_ADD
(
@ -192,7 +208,6 @@ create or replace package body PKG_P8PANELS_QE as
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
RRLS_TMP PKG_P8PANELS_QE_BASE.TRLS; -- Буфер для коллекции удаляемых связей
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
@ -218,20 +233,8 @@ create or replace package body PKG_P8PANELS_QE as
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
loop
/* Если атрибут есть в связях (как источник или как приёмник) */
for J in 0 .. 1
loop
RRLS_TMP := PKG_P8PANELS_QE_BASE.TRLS_LIST_BY_ST(RRLS => RRLS,
SSOURCE_TARGET => RENT.RATTRS(I).SID,
NLIST_TYPE => J);
/* То связь должна быть удалена */
if ((RRLS_TMP is not null) and (RRLS_TMP.COUNT > 0)) then
for K in RRLS_TMP.FIRST .. RRLS_TMP.LAST
loop
PKG_P8PANELS_QE_BASE.TRLS_REMOVE(RRLS => RRLS, SID => RRLS_TMP(K).SID);
end loop;
end if;
end loop;
/* Удаляем связи в которых он задействован */
PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RENT.RATTRS(I).SID);
end loop;
end if;
/* Сохраняем обновленный набор сущностей */
@ -265,6 +268,77 @@ create or replace package body PKG_P8PANELS_QE as
end if;
end QUERY_ENT_POSITION_SET;
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
)
is
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Сформируем описание атрибутов */
PKG_P8PANELS_QE_BASE.QUERY_ENT_ATTRS_GET(NRN => NRN, SID => SID, COUT => COUT);
end QUERY_ENT_ATTRS_GET;
/* Установка состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
CATTRS in clob -- Сериализованное описание атрибутов сущности (BASE64 XML)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
NENT_INDEX PKG_STD.TNUMBER; -- Индекс изменяемой сущности
RATTRS PKG_P8PANELS_QE_BASE.TATTRS; -- Коллекция полученных атриубтов
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем существующие сущности */
RENTS := PKG_P8PANELS_QE_BASE.QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Читаем свзяи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Находим изменяемую сущность */
NENT_INDEX := PKG_P8PANELS_QE_BASE.TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID);
if (NENT_INDEX is null) then
P_EXCEPTION(0,
'Сущность с идентификатором "%s" в запросе "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end if;
/* Десериализуем новый набор атрибутов */
RATTRS := PKG_P8PANELS_QE_BASE.TATTRS_FROM_XML_BASE64(CXML => CATTRS,
SCHARSET => PKG_CHARSET.CHARSET_UTF_(),
BADD_ROOT => true);
/* Сбросим текущие атрибуты сущности */
RENTS(NENT_INDEX).RATTRS := PKG_P8PANELS_QE_BASE.TATTRS();
/* Наполним полученными и зачистим связи */
if (RATTRS is not null) and (RATTRS.COUNT > 0) then
for I in RATTRS.FIRST .. RATTRS.LAST
loop
/* Если атрибут используется - кладём в обновленную коллекцию атрибутов сущности */
if (RATTRS(I).NUSE = 1) then
RENTS(NENT_INDEX).RATTRS.EXTEND();
RENTS(NENT_INDEX).RATTRS(RENTS(NENT_INDEX).RATTRS.LAST) := RATTRS(I);
else
/* Атрибут не используется - необходимо очистись связи в которых он задействован */
PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RATTRS(I).SID);
end if;
end loop;
end if;
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
end QUERY_ENT_ATTRS_SET;
/* Добавление связи в запрос */
procedure QUERY_RL_ADD
(
@ -337,6 +411,6 @@ create or replace package body PKG_P8PANELS_QE as
/* Базовая установка признака публичности */
PKG_P8PANELS_QE_BASE.QUERY_PBL_SET(NRN => NRN, NPBL => NPBL);
end QUERY_PBL_SET;
end PKG_P8PANELS_QE;
/

View File

@ -1,16 +1,16 @@
create or replace package PKG_P8PANELS_QE_BASE as
/* Константы - Типы сущностей */
SENT_TYPE_TABLE constant PKG_STD.TSTRING := 'TABLE'; -- Таблица
SENT_TYPE_VIEW constant PKG_STD.TSTRING := 'VIEW'; -- Представление
/* Типы данных - Атрибут сущности */
type TATTR is record
(
SID PKG_STD.TSTRING, -- Уникальный идентификатор в запросе
SNAME PKG_STD.TSTRING, -- Имя
STITLE PKG_STD.TSTRING, -- Заголовок
NDATA_TYPE PKG_STD.TNUMBER -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
NDATA_TYPE PKG_STD.TNUMBER, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
SAGG PKG_STD.TSTRING, -- Агрегатная функция
SALIAS PKG_STD.TSTRING, -- Псевдоним в выборке
NUSE PKG_STD.TNUMBER, -- Флаг применения в запросе (1 - да, 0 - нет)
NSHOW PKG_STD.TNUMBER -- Флаг отображения в выборке (1 - да, 0 - нет)
);
/* Типы данных - Коллекция атрибутов сущности */
@ -42,6 +42,14 @@ create or replace package PKG_P8PANELS_QE_BASE as
/* Типы данных - Коллекция отношений */
type TRLS is table of TRL;
/* Десериализация коллекции атрибутов сущности (из BASE64) */
function TATTRS_FROM_XML_BASE64
(
CXML in clob, -- XML-описание коллекции атрибутов сущности (BASE64)
SCHARSET in varchar2, -- Кодировка, в которой XML-данные были упакованы в BASE64
BADD_ROOT in boolean := false -- Флаг необходимости добавления корневого тэга (false - нет, true - да)
) return TATTRS; -- Коллекция атрибутов сущности
/* Поиск индекса сущности по идентификатору */
function TENTS_INDEX_BY_ID
(
@ -73,14 +81,6 @@ create or replace package PKG_P8PANELS_QE_BASE as
NY in number -- Координата по оси ординат
);
/* Формирование коллекции связей по источнику/приёмнику */
function TRLS_LIST_BY_ST
(
RRLS in TRLS, -- Коллекция связей
SSOURCE_TARGET in varchar2, -- Идентификатор источника/приёмкника
NLIST_TYPE in number -- Тип формирования коллекции (0 - по источнику, 1 - по приёмнику
) return TRLS; -- Сформированная коллекция
/* Добавление связи в коллекцию */
procedure TRLS_APPEND
(
@ -96,6 +96,13 @@ create or replace package PKG_P8PANELS_QE_BASE as
SID in varchar2 -- Идентификатор удялемой связи
);
/* Подчистка коллекции связей по атрибуту */
procedure TRLS_CLEANUP_BY_ATTR
(
RRLS in out nocopy TRLS, -- Изменяемая коллекция
SATTR_ID in varchar2 -- Идентификатор атрибута
);
/* Считывание записи запроса */
function QUERY_GET
(
@ -177,6 +184,14 @@ create or replace package PKG_P8PANELS_QE_BASE as
RENTS in TENTS -- Коллекция сущностей
);
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
);
/* Чтение связей запроса */
function QUERY_RLS_GET
(
@ -208,6 +223,10 @@ end PKG_P8PANELS_QE_BASE;
/
create or replace package body PKG_P8PANELS_QE_BASE as
/* Константы - Типы сущностей */
SENT_TYPE_TABLE constant PKG_STD.TSTRING := 'TABLE'; -- Таблица
SENT_TYPE_VIEW constant PKG_STD.TSTRING := 'VIEW'; -- Представление
/* Константы - Теги для сериализации */
STAG_DATA constant PKG_STD.TSTRING := 'XDATA'; -- Данные
STAG_QUERIES constant PKG_STD.TSTRING := 'XQUERIES'; -- Запросы
@ -238,6 +257,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
SATTR_Y constant PKG_STD.TSTRING := 'y'; -- Координата по Y
SATTR_SOURCE constant PKG_STD.TSTRING := 'source'; -- Источник
SATTR_TARGET constant PKG_STD.TSTRING := 'target'; -- Приёмник
SATTR_AGG constant PKG_STD.TSTRING := 'agg'; -- Агрегатная функция
SATTR_ALIAS constant PKG_STD.TSTRING := 'alias'; -- Псевдоним
SATTR_USE constant PKG_STD.TSTRING := 'use'; -- Применение в запросе
SATTR_SHOW constant PKG_STD.TSTRING := 'show'; -- Отображение в запросе
/* Получение заголовка представления из метаданных */
function DMSCLVIEWS_TITLE_GET
@ -328,6 +351,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RATTR.SNAME);
PKG_XFAST.ATTR(SNAME => SATTR_TITLE, SVALUE => RATTR.STITLE);
PKG_XFAST.ATTR(SNAME => SATTR_DATA_TYPE, NVALUE => RATTR.NDATA_TYPE);
PKG_XFAST.ATTR(SNAME => SATTR_AGG, SVALUE => RATTR.SAGG);
PKG_XFAST.ATTR(SNAME => SATTR_ALIAS, SVALUE => RATTR.SALIAS);
PKG_XFAST.ATTR(SNAME => SATTR_USE, NVALUE => RATTR.NUSE);
PKG_XFAST.ATTR(SNAME => SATTR_SHOW, NVALUE => RATTR.NSHOW);
/* Закрываем описание атрибута сущности */
PKG_XFAST.UP();
end TATTR_TO_XML;
@ -356,6 +383,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
RRES.SNAME := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_NAME);
RRES.STITLE := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_TITLE);
RRES.NDATA_TYPE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_DATA_TYPE);
RRES.SAGG := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_AGG);
RRES.SALIAS := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_ALIAS);
RRES.NUSE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_USE);
RRES.NSHOW := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_SHOW);
/* Освободим документ */
PKG_XPATH.FREE(RDOCUMENT => XDOC);
exception
@ -371,6 +402,119 @@ create or replace package body PKG_P8PANELS_QE_BASE as
return RRES;
end TATTR_FROM_XML;
/* Поиск индекса атрибута по имени */
function TATTRS_INDEX_BY_NAME
(
RATTRS in TATTRS, -- Коллекция атрибутов
SNAME in varchar2 -- Искомое имя
) return number -- Индекс найденного атрибута (null - если не найдено)
is
begin
/* Обходим коллекцию */
if ((RATTRS is not null) and (RATTRS.COUNT > 0)) then
for I in RATTRS.FIRST .. RATTRS.LAST
loop
begin
/* Возвращаем найденный индекс */
if (RATTRS(I).SNAME = SNAME) then
return I;
end if;
exception
when NO_DATA_FOUND then
null;
end;
end loop;
end if;
/* Ничего не нашли */
return null;
end TATTRS_INDEX_BY_NAME;
/* Формирование списка атрибутов сущности по типу и наименованию */
function TATTRS_MAKE
(
RENT in TENT, -- Родительская сущность
NCOUNT in number := null -- Количество атрибутов (null - все)
) return TATTRS -- Коллекция атрибутов сущности
is
RRES TATTRS; -- Буфер для результата
RVIEW_FIELDS PKG_OBJECT_DESC.TCOLUMNS; -- Коллекция описаний полей представления
RVIEW_FIELD PKG_OBJECT_DESC.TCOLUMN; -- Описание поля представления
NATTR_INDEX PKG_STD.TNUMBER; -- Индекс поля в коллекции атрибутов сущности
BINIT boolean := false; -- Признак инициализации атрибутов сущности
begin
/* Проверим корректность типа сущности */
if (RENT.STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
P_EXCEPTION(0,
'Сущности типа "%s" не поддерживаются.',
COALESCE(RENT.STYPE, '<НЕ УКАЗАН>'));
end if;
/* Проверим, что у сущности задан идентификатор */
if (RENT.SID is null) then
P_EXCEPTION(0,
'Ошибка формирования атрибутов - не задан идентификатор сущности.');
end if;
/* Проверим, что у сущности задано имя */
if (RENT.SNAME is null) then
P_EXCEPTION(0,
'Ошибка формирования атрибутов - не задано имя сущности.');
end if;
/* Инициализируем результат */
RRES := TATTRS();
/* Установим флаг инициализации - в сущности нет атрибутов и нас просили ограничить количество */
if (((RENT.RATTRS is null) or (RENT.RATTRS.COUNT = 0)) and (NCOUNT is not null)) then
BINIT := true;
end if;
/* Если сущность это представление */
if (RENT.STYPE = SENT_TYPE_VIEW) then
/* Получим список полей представления */
RVIEW_FIELDS := PKG_OBJECT_DESC.DESC_SEL_COLUMNS(SSELECT_NAME => RENT.SNAME, BRAISE_ERROR => true);
/* Собираем атрибуты в ответ */
for I in 1 .. PKG_OBJECT_DESC.COUNT_COLUMNS(RCOLUMNS => RVIEW_FIELDS)
loop
/* Считываем очередное поле из коллекции описания полей представления */
RVIEW_FIELD := PKG_OBJECT_DESC.FETCH_COLUMN(RCOLUMNS => RVIEW_FIELDS, IINDEX => I);
/* Если поле поддерживаемого типа */
if (RVIEW_FIELD.DATA_TYPE in (PKG_STD.DATA_TYPE_STR(), PKG_STD.DATA_TYPE_NUM(), PKG_STD.DATA_TYPE_DATE())) then
/* Добавляем элемент в результирующую коллекцию */
RRES.EXTEND();
/* Ищем такой атрибут в родительской сущности */
NATTR_INDEX := TATTRS_INDEX_BY_NAME(RATTRS => RENT.RATTRS, SNAME => RVIEW_FIELD.COLUMN_NAME);
/* Если это поле уже есть в коллекции атрибутов родительской сущности */
if (NATTR_INDEX is not null) then
/* Возьмём элемент оттуда */
RRES(RRES.LAST) := RENT.RATTRS(NATTR_INDEX);
else
/* Такого элемента нет - берем из представления */
RRES(RRES.LAST).SID := TATTR_ID_MAKE(SENT_ID => RENT.SID, SNAME => RVIEW_FIELD.COLUMN_NAME);
RRES(RRES.LAST).SNAME := RVIEW_FIELD.COLUMN_NAME;
RRES(RRES.LAST).STITLE := DMSCLVIEWSATTRS_TITLE_GET(SVIEW_NAME => RENT.SNAME,
SATTR_NAME => RRES(RRES.LAST).SNAME);
RRES(RRES.LAST).NDATA_TYPE := RVIEW_FIELD.DATA_TYPE;
/* Если это инициализиция сущности - установим флаг применения в запросе (тогда атрибут будет отображен в диаграмме) */
if (BINIT) then
RRES(RRES.LAST).NUSE := 1;
RRES(RRES.LAST).NSHOW := 1;
else
RRES(RRES.LAST).NUSE := 0;
RRES(RRES.LAST).NSHOW := 0;
end if;
end if;
/* Ограничим объем коллекции если необходимо */
if (NCOUNT is not null) then
exit when RRES.LAST = NCOUNT;
end if;
end if;
end loop;
end if;
/* Если сущность это таблица */
if (RENT.STYPE = SENT_TYPE_TABLE) then
P_EXCEPTION(0,
'Поддержка формирования атрибутов для сущностей типа "Таблица" ещё не реализована.');
end if;
/* Возвращаем результат */
return RRES;
end TATTRS_MAKE;
/* Сериализация коллекции атрибутов сущности */
procedure TATTRS_TO_XML
(
@ -439,6 +583,26 @@ create or replace package body PKG_P8PANELS_QE_BASE as
return RRES;
end TATTRS_FROM_XML;
/* Десериализация коллекции атрибутов сущности (из BASE64) */
function TATTRS_FROM_XML_BASE64
(
CXML in clob, -- XML-описание коллекции атрибутов сущности (BASE64)
SCHARSET in varchar2, -- Кодировка, в которой XML-данные были упакованы в BASE64
BADD_ROOT in boolean := false -- Флаг необходимости добавления корневого тэга (false - нет, true - да)
) return TATTRS -- Коллекция атрибутов сущности
is
CTMP clob; -- Буфер для преобразований
begin
/* Избавимся от BASE64 */
CTMP := BLOB2CLOB(LBDATA => BASE64_DECODE(LCSRCE => CXML), SCHARSET => SCHARSET);
/* Если надо - добавим корень */
if (BADD_ROOT) then
CTMP := '<' || STAG_ATTRS || '>' || CTMP || '</' || STAG_ATTRS || '>';
end if;
/* Десериализуем */
return TATTRS_FROM_XML(CXML => CTMP);
end TATTRS_FROM_XML_BASE64;
/* Формирование идентификатора сущности */
function TENT_ID_MAKE
(
@ -462,15 +626,13 @@ create or replace package body PKG_P8PANELS_QE_BASE as
/* Формирование описания сущности */
function TENT_MAKE
(
SNAME in varchar2, -- Имя
STYPE in varchar2, -- Тип (см. константы SENT_TYPE_*)
NNUMB in number -- Номер сущности
) return TENT -- Описание сущности
SNAME in varchar2, -- Имя
STYPE in varchar2, -- Тип (см. константы SENT_TYPE_*)
NNUMB in number -- Номер сущности
) return TENT -- Описание сущности
is
RENT TENT; -- Буфер для результата
RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления
RVIEW_FIELDS PKG_OBJECT_DESC.TCOLUMNS; -- Коллекция описаний полей представления
RVIEW_FIELD PKG_OBJECT_DESC.TCOLUMN; -- Описание поля представления
RENT TENT; -- Буфер для результата
RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления
begin
/* Проверим корректность типа сущности */
if (STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
@ -482,30 +644,13 @@ create or replace package body PKG_P8PANELS_QE_BASE as
if (STYPE = SENT_TYPE_VIEW) then
/* Получим описание представления */
RVIEW := PKG_OBJECT_DESC.DESC_VIEW(SVIEW_NAME => SNAME, BRAISE_ERROR => true);
/* Получим список полей представления */
RVIEW_FIELDS := PKG_OBJECT_DESC.DESC_SEL_COLUMNS(SSELECT_NAME => SNAME, BRAISE_ERROR => true);
/* Собираем заголовок сущности */
RENT.SID := TENT_ID_MAKE(SNAME => RVIEW.VIEW_NAME, NNUMB => NNUMB);
RENT.SNAME := RVIEW.VIEW_NAME;
RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME);
RENT.STYPE := SENT_TYPE_VIEW;
RENT.RATTRS := TATTRS();
/* Собираем атрибуты в ответ */
for I in 1 .. PKG_OBJECT_DESC.COUNT_COLUMNS(RCOLUMNS => RVIEW_FIELDS)
loop
/* По умолчанию - первые 10 */
exit when I > 10;
/* Считываем очередное поле из коллекции описания полей представления */
RVIEW_FIELD := PKG_OBJECT_DESC.FETCH_COLUMN(RCOLUMNS => RVIEW_FIELDS, IINDEX => I);
/* Формируем описание поля и добавляем в коллекцию атрибутов сущности */
RENT.RATTRS.EXTEND();
RENT.RATTRS(RENT.RATTRS.LAST).SID := TATTR_ID_MAKE(SENT_ID => RENT.SID, SNAME => RVIEW_FIELD.COLUMN_NAME);
RENT.RATTRS(RENT.RATTRS.LAST).SNAME := RVIEW_FIELD.COLUMN_NAME;
RENT.RATTRS(RENT.RATTRS.LAST).STITLE := DMSCLVIEWSATTRS_TITLE_GET(SVIEW_NAME => RENT.SNAME,
SATTR_NAME => RENT.RATTRS(RENT.RATTRS.LAST)
.SNAME);
RENT.RATTRS(RENT.RATTRS.LAST).NDATA_TYPE := RVIEW_FIELD.DATA_TYPE;
end loop;
/* Формируем набор атрибутов */
RENT.RATTRS := TATTRS_MAKE(RENT => RENT, NCOUNT => 10);
end if;
/* Если сущность это таблица */
if (STYPE = SENT_TYPE_TABLE) then
@ -945,6 +1090,29 @@ create or replace package body PKG_P8PANELS_QE_BASE as
end if;
end TRLS_REMOVE;
/* Подчистка коллекции связей по атрибуту */
procedure TRLS_CLEANUP_BY_ATTR
(
RRLS in out nocopy TRLS, -- Изменяемая коллекция
SATTR_ID in varchar2 -- Идентификатор атрибута
)
is
RRLS_TMP PKG_P8PANELS_QE_BASE.TRLS; -- Буфер для коллекции удаляемых связей
begin
/* Если атрибут есть в связях (как источник или как приёмник) */
for J in 0 .. 1
loop
RRLS_TMP := TRLS_LIST_BY_ST(RRLS => RRLS, SSOURCE_TARGET => SATTR_ID, NLIST_TYPE => J);
/* То связь должна быть удалена */
if ((RRLS_TMP is not null) and (RRLS_TMP.COUNT > 0)) then
for K in RRLS_TMP.FIRST .. RRLS_TMP.LAST
loop
TRLS_REMOVE(RRLS => RRLS, SID => RRLS_TMP(K).SID);
end loop;
end if;
end loop;
end TRLS_CLEANUP_BY_ATTR;
/* Сериализация коллекции связей */
procedure TRLS_TO_XML
(
@ -1483,6 +1651,56 @@ create or replace package body PKG_P8PANELS_QE_BASE as
/* Обновим дату изменения запроса */
QUERY_CH_DATE_SYNC(NRN => NRN);
end QUERY_ENTS_SET;
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
NENT_INDEX PKG_STD.TNUMBER; -- Индекс изменяемой сущности в коллекции
RENTS TENTS; -- Коллекция существующих сущностей
RENT TENT; -- Изменяемая сущность
RATTRS TATTRS; -- Коллекция атрибутов изменяемой сущности
begin
/* Читаем запись запроса */
RQ := QUERY_GET(NRN => NRN);
/* Читаем существующие сущности */
RENTS := QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Читаем изменяемую сущность */
NENT_INDEX := TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID);
if (NENT_INDEX is not null) then
RENT := RENTS(NENT_INDEX);
else
P_EXCEPTION(0,
'Сущность с идентификатором "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'));
end if;
/* Получим полный набор атрибутов сущности */
RATTRS := TATTRS_MAKE(RENT => RENT);
/* Начинаем формирование XML */
PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_, BALINE => true, BINDENT => true);
/* Открываем корень */
PKG_XFAST.DOWN_NODE(SNAME => STAG_DATA);
/* Формируем описание тарибутов */
TATTRS_TO_XML(RATTRS => RATTRS);
/* Закрываем корень */
PKG_XFAST.UP();
/* Сериализуем */
COUT := PKG_XFAST.SERIALIZE_TO_CLOB();
/* Завершаем формирование XML */
PKG_XFAST.EPILOGUE();
exception
when others then
/* Завершаем формирование XML */
PKG_XFAST.EPILOGUE();
/* Вернем ошибку */
PKG_STATE.DIAGNOSTICS_STACKED();
P_EXCEPTION(0, PKG_STATE.SQL_ERRM());
end QUERY_ENT_ATTRS_GET;
/* Чтение связей запроса */
function QUERY_RLS_GET