Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
fef41f5186 | |||
0d03edbd17 | |||
|
c6d21c83b5 |
@ -105,14 +105,11 @@ const getDisplaySize = () => {
|
|||||||
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
|
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
|
||||||
|
|
||||||
//Конвертация объекта в Base64 XML
|
//Конвертация объекта в Base64 XML
|
||||||
const object2XML = (obj, builderOptions) => {
|
const object2Base64XML = (obj, builderOptions) => {
|
||||||
const builder = new XMLBuilder(builderOptions);
|
const builder = new XMLBuilder(builderOptions);
|
||||||
return builder.build(obj);
|
return btoa(unescape(encodeURIComponent(builder.build(obj))));
|
||||||
};
|
};
|
||||||
|
|
||||||
//Конвертация объекта в Base64 XML
|
|
||||||
const object2Base64XML = (obj, builderOptions) => btoa(unescape(encodeURIComponent(object2XML(obj, builderOptions))));
|
|
||||||
|
|
||||||
//Конвертация XML в JSON
|
//Конвертация XML в JSON
|
||||||
const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => {
|
const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -203,7 +200,6 @@ export {
|
|||||||
hasValue,
|
hasValue,
|
||||||
getDisplaySize,
|
getDisplaySize,
|
||||||
deepCopyObject,
|
deepCopyObject,
|
||||||
object2XML,
|
|
||||||
object2Base64XML,
|
object2Base64XML,
|
||||||
xml2JSON,
|
xml2JSON,
|
||||||
formatDateRF,
|
formatDateRF,
|
||||||
|
316
app/panels/clnt_task_board/clnt_task_board.js
Normal file
316
app/panels/clnt_task_board/clnt_task_board.js
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
174
app/panels/clnt_task_board/components/custom_input_field.js
Normal file
174
app/panels/clnt_task_board/components/custom_input_field.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
336
app/panels/clnt_task_board/components/filter_dialog.js
Normal file
336
app/panels/clnt_task_board/components/filter_dialog.js
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
99
app/panels/clnt_task_board/components/note_dialog.js
Normal file
99
app/panels/clnt_task_board/components/note_dialog.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
131
app/panels/clnt_task_board/components/settings_dialog.js
Normal file
131
app/panels/clnt_task_board/components/settings_dialog.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
182
app/panels/clnt_task_board/components/status_card.js
Normal file
182
app/panels/clnt_task_board/components/status_card.js
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
109
app/panels/clnt_task_board/components/status_card_settings.js
Normal file
109
app/panels/clnt_task_board/components/status_card_settings.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
376
app/panels/clnt_task_board/components/task_card.js
Normal file
376
app/panels/clnt_task_board/components/task_card.js
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
158
app/panels/clnt_task_board/components/task_form.js
Normal file
158
app/panels/clnt_task_board/components/task_form.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
155
app/panels/clnt_task_board/components/task_form_tab_executor.js
Normal file
155
app/panels/clnt_task_board/components/task_form_tab_executor.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
192
app/panels/clnt_task_board/components/task_form_tab_info.js
Normal file
192
app/panels/clnt_task_board/components/task_form_tab_info.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
169
app/panels/clnt_task_board/components/task_form_tab_props.js
Normal file
169
app/panels/clnt_task_board/components/task_form_tab_props.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
212
app/panels/clnt_task_board/filter.js
Normal file
212
app/panels/clnt_task_board/filter.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
255
app/panels/clnt_task_board/hooks/dict_hooks.js
Normal file
255
app/panels/clnt_task_board/hooks/dict_hooks.js
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
122
app/panels/clnt_task_board/hooks/filter_hooks.js
Normal file
122
app/panels/clnt_task_board/hooks/filter_hooks.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
243
app/panels/clnt_task_board/hooks/hooks.js
Normal file
243
app/panels/clnt_task_board/hooks/hooks.js
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
191
app/panels/clnt_task_board/hooks/task_dialog_hooks.js
Normal file
191
app/panels/clnt_task_board/hooks/task_dialog_hooks.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
431
app/panels/clnt_task_board/hooks/tasks_hooks.js
Normal file
431
app/panels/clnt_task_board/hooks/tasks_hooks.js
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
16
app/panels/clnt_task_board/index.js
Normal file
16
app/panels/clnt_task_board/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Парус 8 - Панели мониторинга - УДП - Доски задач
|
||||||
|
Панель мониторинга: Точка входа
|
||||||
|
*/
|
||||||
|
|
||||||
|
//---------------------
|
||||||
|
//Подключение библиотек
|
||||||
|
//---------------------
|
||||||
|
|
||||||
|
import { ClntTaskBoard } from "./clnt_task_board"; //Корневая панель выполнения работ
|
||||||
|
|
||||||
|
//----------------
|
||||||
|
//Интерфейс модуля
|
||||||
|
//----------------
|
||||||
|
|
||||||
|
export const RootClass = ClntTaskBoard;
|
294
app/panels/clnt_task_board/layouts.js
Normal file
294
app/panels/clnt_task_board/layouts.js
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
Парус 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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
48
app/panels/clnt_task_board/styles.js
Normal file
48
app/panels/clnt_task_board/styles.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 }
|
||||||
|
};
|
180
app/panels/clnt_task_board/task_dialog.js
Normal file
180
app/panels/clnt_task_board/task_dialog.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
Парус 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 };
|
@ -19,12 +19,11 @@ import { DATA_TYPE } from "../../common"; //Общие ресурсы и кон
|
|||||||
|
|
||||||
//Типовые цвета точек привязки
|
//Типовые цвета точек привязки
|
||||||
const HANDLE_BORDER_COLOR = "#69db7c";
|
const HANDLE_BORDER_COLOR = "#69db7c";
|
||||||
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
|
|
||||||
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
|
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
|
||||||
|
|
||||||
//Стили
|
//Стили
|
||||||
const STYLES = {
|
const STYLES = {
|
||||||
CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
|
CONTAINER: { display: "flex", width: "100%", height: "100%" },
|
||||||
HANDLE_SOURCE: isConnecting => ({
|
HANDLE_SOURCE: isConnecting => ({
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
@ -33,18 +32,17 @@ const STYLES = {
|
|||||||
borderRadius: 7,
|
borderRadius: 7,
|
||||||
background: "white"
|
background: "white"
|
||||||
}),
|
}),
|
||||||
HANDLE_TARGET: (isConnecting, isValidConnection) => ({
|
HANDLE_TARGET: isConnecting => ({
|
||||||
width: isConnecting ? 14 : 0,
|
width: isConnecting ? 14 : 0,
|
||||||
height: 14,
|
height: 14,
|
||||||
left: isConnecting ? -7 : 0,
|
left: isConnecting ? -7 : 0,
|
||||||
border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
|
border: `2px solid ${HANDLE_BORDER_COLOR}`,
|
||||||
borderRadius: 7,
|
borderRadius: 7,
|
||||||
background: "white",
|
background: "white",
|
||||||
visibility: isConnecting ? "visible" : "hidden"
|
visibility: isConnecting ? "visible" : "hidden"
|
||||||
}),
|
}),
|
||||||
CONTENT_STACK: { width: "100%" },
|
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" }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//Иконки
|
//Иконки
|
||||||
@ -57,36 +55,11 @@ const ICONS = {
|
|||||||
|
|
||||||
//Структура данных об атрибуте сущности
|
//Структура данных об атрибуте сущности
|
||||||
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
|
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
parentEntity: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
|
dataType: PropTypes.number.isRequired
|
||||||
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"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
//Тело модуля
|
//Тело модуля
|
||||||
//-----------
|
//-----------
|
||||||
@ -94,45 +67,25 @@ const attrGetShow = (attr, callToAction = false) => {
|
|||||||
//Атрибут сущности
|
//Атрибут сущности
|
||||||
const Attribute = ({ data }) => {
|
const Attribute = ({ data }) => {
|
||||||
//Поиск идентификатора соединяемого элемента
|
//Поиск идентификатора соединяемого элемента
|
||||||
const [connectionNodeId, targetConnectionNode, connectionStatus] = useStore(state => [
|
const connectionNodeId = useStore(state => state.connectionNodeId);
|
||||||
state.connectionNodeId,
|
|
||||||
state?.connectionEndHandle?.nodeId,
|
|
||||||
state.connectionStatus
|
|
||||||
]);
|
|
||||||
|
|
||||||
//Флаг выполнения соединения сущностей
|
//Флаг выполнения соединения сущностей
|
||||||
const isConnecting = Boolean(connectionNodeId);
|
const isConnecting = Boolean(connectionNodeId);
|
||||||
|
|
||||||
//Флаг корректности соединения сущностей
|
|
||||||
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
|
|
||||||
|
|
||||||
//Получим атрибуты состояния отображения
|
|
||||||
const [showTitle, showIcon] = attrGetShow(data);
|
|
||||||
|
|
||||||
//Формирование представления
|
//Формирование представления
|
||||||
return (
|
return (
|
||||||
<Box p={1} sx={STYLES.CONTAINER}>
|
<Box p={1} sx={STYLES.CONTAINER}>
|
||||||
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
|
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
|
||||||
<Handle
|
<Handle type={"target"} position={Position.Left} isConnectableStart={false} style={STYLES.HANDLE_TARGET(isConnecting)} />
|
||||||
type={"target"}
|
|
||||||
position={Position.Left}
|
|
||||||
isConnectableStart={false}
|
|
||||||
style={STYLES.HANDLE_TARGET(isConnecting, isValidConnection)}
|
|
||||||
/>
|
|
||||||
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
|
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
|
||||||
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
|
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
|
||||||
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
|
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
|
||||||
<Typography variant={"body2"} noWrap title={data.title}>
|
<Typography variant={"body2"} noWrap title={data.title}>
|
||||||
{data.title}
|
{data.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction={"row"} alignItems={"center"} spacing={0.5}>
|
<Typography variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
|
||||||
<Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
|
{data.name}
|
||||||
{`${data.name},`}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
|
|
||||||
{showIcon}
|
|
||||||
</Icon>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@ -148,4 +101,4 @@ Attribute.propTypes = {
|
|||||||
//Интерфейс модуля
|
//Интерфейс модуля
|
||||||
//----------------
|
//----------------
|
||||||
|
|
||||||
export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow };
|
export { Attribute };
|
||||||
|
@ -17,7 +17,6 @@ import "./entity.css"; //Стили компомнента
|
|||||||
|
|
||||||
//Структура данных о сущности запроса
|
//Структура данных о сущности запроса
|
||||||
const ENTITY_DATA_SHAPE = PropTypes.shape({
|
const ENTITY_DATA_SHAPE = PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired
|
title: PropTypes.string.isRequired
|
||||||
});
|
});
|
||||||
@ -27,7 +26,7 @@ const ENTITY_DATA_SHAPE = PropTypes.shape({
|
|||||||
//-----------
|
//-----------
|
||||||
|
|
||||||
//Сущность запроса
|
//Сущность запроса
|
||||||
const Entity = ({ data, selected = false }) => {
|
const Entity = ({ data, selected }) => {
|
||||||
return (
|
return (
|
||||||
<div className="entity__wrapper" data-selected={selected}>
|
<div className="entity__wrapper" data-selected={selected}>
|
||||||
<div className="entity__title">
|
<div className="entity__title">
|
||||||
@ -48,4 +47,4 @@ Entity.propTypes = {
|
|||||||
//Интерфейс модуля
|
//Интерфейс модуля
|
||||||
//----------------
|
//----------------
|
||||||
|
|
||||||
export { Entity, ENTITY_DATA_SHAPE };
|
export { Entity };
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
Парус 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 };
|
|
@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
Парус 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 };
|
|
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
Парус 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 };
|
|
@ -10,7 +10,6 @@
|
|||||||
import React from "react"; //Классы React
|
import React from "react"; //Классы React
|
||||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||||
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
|
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||||
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
|
|
||||||
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
||||||
|
|
||||||
//---------
|
//---------
|
||||||
@ -21,15 +20,14 @@ import { BUTTONS } from "../../../../../app.text"; //Общие текстовы
|
|||||||
const STYLES = {
|
const STYLES = {
|
||||||
SMALL_TOOL_ICON: {
|
SMALL_TOOL_ICON: {
|
||||||
fontSize: 20
|
fontSize: 20
|
||||||
},
|
}
|
||||||
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//---------
|
//---------
|
||||||
//Константы
|
//Константы
|
||||||
//---------
|
//---------
|
||||||
|
|
||||||
//Структура элемента списка запросов
|
//Структура данных о сущности запроса
|
||||||
const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
|
const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
|
||||||
rn: PropTypes.number.isRequired,
|
rn: PropTypes.number.isRequired,
|
||||||
code: PropTypes.string.isRequired,
|
code: PropTypes.string.isRequired,
|
||||||
@ -45,7 +43,7 @@ const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
|
|||||||
//Тело модуля
|
//Тело модуля
|
||||||
//-----------
|
//-----------
|
||||||
|
|
||||||
//Список запросов
|
//Диалог открытия запроса
|
||||||
const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = null, onReady = null, onEdit = null, onDelete = null } = {}) => {
|
const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = null, onReady = null, onEdit = null, onDelete = null } = {}) => {
|
||||||
//При выборе элемента списка
|
//При выборе элемента списка
|
||||||
const handleSelectClick = query => {
|
const handleSelectClick = query => {
|
||||||
@ -78,7 +76,7 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
|
|||||||
|
|
||||||
//Формирование представления
|
//Формирование представления
|
||||||
return (
|
return (
|
||||||
<List sx={STYLES.LIST}>
|
<List sx={{ height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" }}>
|
||||||
{queries.map((query, i) => {
|
{queries.map((query, i) => {
|
||||||
const selected = query.rn === current;
|
const selected = query.rn === current;
|
||||||
const disabled = !query.modify;
|
const disabled = !query.modify;
|
||||||
@ -87,8 +85,8 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
|
|||||||
const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
|
const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
|
||||||
const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch";
|
const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch";
|
||||||
return (
|
return (
|
||||||
<ListItem key={i} disablePadding>
|
<ListItem key={i}>
|
||||||
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected} dense>
|
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={query.name}
|
primary={query.name}
|
||||||
secondaryTypographyProps={{ component: "div" }}
|
secondaryTypographyProps={{ component: "div" }}
|
||||||
|
@ -36,7 +36,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null
|
|||||||
const handleQueryAdd = () => setModQuery(true);
|
const handleQueryAdd = () => setModQuery(true);
|
||||||
|
|
||||||
//При выборе запроса
|
//При выборе запроса
|
||||||
const handleQuerySelect = query => onQuerySelect && onQuerySelect({ ...query });
|
const handleQuerySelect = query => onQuerySelect && onQuerySelect(query.rn);
|
||||||
|
|
||||||
//При установке признака публичности
|
//При установке признака публичности
|
||||||
const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1);
|
const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1);
|
||||||
|
@ -8,11 +8,10 @@
|
|||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from "react"; //Классы React
|
import React, { useState, useCallback, useEffect } from "react"; //Классы React
|
||||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
|
||||||
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
|
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
|
||||||
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
|
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
|
||||||
import { Entity, ENTITY_DATA_SHAPE } from "../entity/entity"; //Сущность запроса
|
import { Entity } from "../entity/entity"; //Сущность запроса
|
||||||
import { Attribute, ATTRIBUTE_DATA_SHAPE } from "../attribute/attribute"; //Атрибут сущности
|
import { Attribute } from "../attribute/attribute"; //Атрибут сущности
|
||||||
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
|
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
|
||||||
import "./query_diagram.css"; //Стили компонента
|
import "./query_diagram.css"; //Стили компонента
|
||||||
|
|
||||||
@ -37,48 +36,42 @@ const NODE_TYPES_COMPONENTS = {
|
|||||||
[NODE_TYPE.ATTRIBUTE]: Attribute
|
[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()) => {
|
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);
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
//Проверка корректности связи
|
|
||||||
const isValidConnection = (connection, nodes, edges) => {
|
const isValidConnection = (connection, nodes, edges) => {
|
||||||
//Должны быть заданы источник и приёмник
|
if (!connection.source || !connection.target) {
|
||||||
if (!connection.source || !connection.target) return false;
|
return false;
|
||||||
//Найдем источник и приёмник
|
}
|
||||||
const source = nodes.find(node => node.id === connection.source);
|
|
||||||
|
const tableId = connection.source.split("-")[0];
|
||||||
|
const isSameTable = connection.target.startsWith(tableId);
|
||||||
|
if (isSameTable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const target = nodes.find(node => node.id === connection.target);
|
const target = nodes.find(node => node.id === connection.target);
|
||||||
//Приёмник и источник должны существовать
|
if (!target || target.id === connection.source) {
|
||||||
if (!target || !source) return false;
|
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);
|
return !hasCycle(connection, target, nodes, edges);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,18 +80,7 @@ const isValidConnection = (connection, nodes, edges) => {
|
|||||||
//-----------
|
//-----------
|
||||||
|
|
||||||
//Диаграмма запроса
|
//Диаграмма запроса
|
||||||
const QueryDiagram = ({
|
const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRemove, onRelationAdd, onRelationRemove }) => {
|
||||||
entities = [],
|
|
||||||
relations = [],
|
|
||||||
onEntityClick,
|
|
||||||
onEntityAttrClick,
|
|
||||||
onEntityPositionChange,
|
|
||||||
onEntityRemove,
|
|
||||||
onRelactionClick,
|
|
||||||
onRelationAdd,
|
|
||||||
onRelationRemove,
|
|
||||||
onPaneClick
|
|
||||||
}) => {
|
|
||||||
//Собственное состояние - элементы
|
//Собственное состояние - элементы
|
||||||
const [nodes, setNodes] = useState(entities);
|
const [nodes, setNodes] = useState(entities);
|
||||||
|
|
||||||
@ -111,44 +93,17 @@ const QueryDiagram = ({
|
|||||||
//При изменении элементов на диаграмме
|
//При изменении элементов на диаграмме
|
||||||
const handleNodesChange = useCallback(
|
const handleNodesChange = useCallback(
|
||||||
changes => {
|
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)
|
if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
|
||||||
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
|
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
|
||||||
//Если закончили двигать сущность и ранее запомнили начало движения - вызываем колбэки для нажатия и двиения сущности
|
|
||||||
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
|
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
|
||||||
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
|
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
|
||||||
if (onEntityClick) onEntityClick(movedNode.id);
|
|
||||||
setMovedNode(null);
|
setMovedNode(null);
|
||||||
}
|
}
|
||||||
//Если удалили сущность - вызываем колбэк для удаления
|
|
||||||
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
|
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
|
||||||
onEntityRemove(changes[0].id);
|
onEntityRemove(changes[0].id);
|
||||||
},
|
},
|
||||||
[movedNode, entities, onEntityClick, onEntityPositionChange, onEntityRemove]
|
[movedNode, entities, onEntityPositionChange, onEntityRemove]
|
||||||
);
|
|
||||||
|
|
||||||
//При выборе элемента диаграммы
|
|
||||||
const handleNodeClick = useCallback(
|
|
||||||
(e, node) =>
|
|
||||||
node?.type == NODE_TYPE.ENTITY
|
|
||||||
? onEntityClick && onEntityClick(node?.id)
|
|
||||||
: onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
|
|
||||||
[onEntityClick, onEntityAttrClick]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
//При связывании элементов на диаграмме
|
//При связывании элементов на диаграмме
|
||||||
@ -157,9 +112,6 @@ const QueryDiagram = ({
|
|||||||
onRelationAdd && onRelationAdd(connection.source, connection.target);
|
onRelationAdd && onRelationAdd(connection.source, connection.target);
|
||||||
};
|
};
|
||||||
|
|
||||||
//При выборе связи диаграммы
|
|
||||||
const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);
|
|
||||||
|
|
||||||
//При изменении связей на диаграмме
|
//При изменении связей на диаграмме
|
||||||
const handleEdgesChange = useCallback(
|
const handleEdgesChange = useCallback(
|
||||||
changes => {
|
changes => {
|
||||||
@ -169,13 +121,9 @@ const QueryDiagram = ({
|
|||||||
[onRelationRemove]
|
[onRelationRemove]
|
||||||
);
|
);
|
||||||
|
|
||||||
//При нажатии на холст диаграммы
|
const validateConnection = connection => {
|
||||||
const handlePaneClick = () => onPaneClick && onPaneClick();
|
return isValidConnection(connection, nodes, edges);
|
||||||
|
};
|
||||||
//Валидация связи
|
|
||||||
const validateConnection = connection => isValidConnection(connection, nodes, edges);
|
|
||||||
|
|
||||||
//Подсветка выбранной сущности
|
|
||||||
|
|
||||||
//При изменении состава сущностей
|
//При изменении состава сущностей
|
||||||
useEffect(() => setNodes(entities), [entities]);
|
useEffect(() => setNodes(entities), [entities]);
|
||||||
@ -189,11 +137,8 @@ const QueryDiagram = ({
|
|||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
nodeTypes={NODE_TYPES_COMPONENTS}
|
nodeTypes={NODE_TYPES_COMPONENTS}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodeClick={handleNodeClick}
|
|
||||||
onNodesChange={handleNodesChange}
|
onNodesChange={handleNodesChange}
|
||||||
onEdgeClick={handleEdgeClick}
|
|
||||||
onEdgesChange={handleEdgesChange}
|
onEdgesChange={handleEdgesChange}
|
||||||
onPaneClick={handlePaneClick}
|
|
||||||
defaultEdgeOptions={{
|
defaultEdgeOptions={{
|
||||||
animated: true,
|
animated: true,
|
||||||
style: STYLES.EDGE
|
style: STYLES.EDGE
|
||||||
@ -208,20 +153,6 @@ const QueryDiagram = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
//Контроль свойств компонента - Диаграмма запроса
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
//----------------
|
//----------------
|
||||||
//Интерфейс модуля
|
//Интерфейс модуля
|
||||||
//----------------
|
//----------------
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
Парус 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 };
|
|
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Парус 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 };
|
|
@ -7,23 +7,22 @@
|
|||||||
//Подключение библиотек
|
//Подключение библиотек
|
||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
import React, { useState, useContext } from "react"; //Классы React
|
import React, { useState } from "react"; //Классы React
|
||||||
import { Box, Grid } from "@mui/material"; //Интерфейсные компоненты MUI
|
import { Box, Grid, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||||
import { ApplicationСtx } from "../../context/application"; //Контекст взаимодействия с приложением
|
|
||||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола
|
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола
|
||||||
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
|
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
|
||||||
|
import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы приложения
|
||||||
import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса
|
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 { 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"; //Пользовательские хуки
|
import { useQueryDesc } from "./hooks"; //Пользовательские хуки
|
||||||
|
|
||||||
//---------
|
//---------
|
||||||
//Константы
|
//Константы
|
||||||
//---------
|
//---------
|
||||||
|
|
||||||
//Заголовок панели по умолчанию
|
|
||||||
const APP_BAR_TITLE_DEFAULT = "Редактор запросов";
|
|
||||||
|
|
||||||
//Стили
|
//Стили
|
||||||
const STYLES = {
|
const STYLES = {
|
||||||
CONTAINER: { display: "flex" },
|
CONTAINER: { display: "flex" },
|
||||||
@ -40,79 +39,26 @@ const QueryEditor = () => {
|
|||||||
//Текущий запрос
|
//Текущий запрос
|
||||||
const [query, setQuery] = useState(null);
|
const [query, setQuery] = useState(null);
|
||||||
|
|
||||||
//Текущая сущность
|
|
||||||
const [entity, setEntity] = useState(null);
|
|
||||||
|
|
||||||
//Текущая связь
|
|
||||||
const [relation, setRelation] = useState(null);
|
|
||||||
|
|
||||||
//Отображения менеджера запросов
|
//Отображения менеджера запросов
|
||||||
const [openQueriesManager, setOpenQueriesManager] = useState(true);
|
const [openQueriesManager, setOpenQueriesManager] = useState(true);
|
||||||
|
|
||||||
|
//Отображение диалога добавления сущности
|
||||||
|
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
|
||||||
|
|
||||||
//Получение данных запроса
|
//Получение данных запроса
|
||||||
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh] = useQueryDesc(query);
|
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl] = 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 handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y);
|
||||||
|
|
||||||
//Обработка добавления сущности в запрос
|
|
||||||
const handleEntityAdd = async (entName, cb) => {
|
|
||||||
await addEnt(entName, "VIEW");
|
|
||||||
cb(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
//Обработка удаления сущности из запроса
|
//Обработка удаления сущности из запроса
|
||||||
const handleEntityRemove = async ent => {
|
const handleEntityRemove = ent => removeEnt(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ущностей
|
//Обработка добавления отношения cущностей
|
||||||
const handleRelationAdd = (source, target) => {
|
const handleRelationAdd = (source, target) => addRl(source, target);
|
||||||
cleanupEnRlSelection();
|
|
||||||
addRl(source, target);
|
|
||||||
};
|
|
||||||
|
|
||||||
//Обработка удаления отношения cущностей
|
//Обработка удаления отношения cущностей
|
||||||
const handleRelationRemove = async rl => {
|
const handleRelationRemove = rl => removeRl(rl);
|
||||||
await removeRl(rl);
|
|
||||||
if (relation && relation?.id === rl) cleanupEnRlSelection();
|
|
||||||
};
|
|
||||||
|
|
||||||
//При нажатии на панели (пустом месте) диаграммы запроса
|
|
||||||
const handlePaneClick = () => cleanupEnRlSelection();
|
|
||||||
|
|
||||||
//Открытие менеджера запросов
|
//Открытие менеджера запросов
|
||||||
const handleOpenQueriesManager = () => setOpenQueriesManager(true);
|
const handleOpenQueriesManager = () => setOpenQueriesManager(true);
|
||||||
@ -121,25 +67,26 @@ const QueryEditor = () => {
|
|||||||
const handleCancelQueriesManager = () => setOpenQueriesManager(false);
|
const handleCancelQueriesManager = () => setOpenQueriesManager(false);
|
||||||
|
|
||||||
//Закрытие запроса
|
//Закрытие запроса
|
||||||
const handleQueryClose = () => {
|
const handleQueryClose = () => setQuery(null);
|
||||||
setAppBarTitle(APP_BAR_TITLE_DEFAULT);
|
|
||||||
cleanupEnRlSelection();
|
|
||||||
setQuery(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
//При выборе запроса
|
//При выборе запроса
|
||||||
const handleQuerySelect = ({ rn, name }) => {
|
const handleQuerySelect = query => {
|
||||||
setAppBarTitle(`Запрос [${name}]`);
|
setQuery(query);
|
||||||
setQuery(rn);
|
|
||||||
setOpenQueriesManager(false);
|
setOpenQueriesManager(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
//При изменении свойств запроса
|
//При добавлении сущности в запрос
|
||||||
const handleQueryOptionsChanged = () => {
|
const handleEntityAdd = () => setOpenEntityAddDialog(true);
|
||||||
cleanupEnRlSelection();
|
|
||||||
doRefresh();
|
//Закрытие диалога добавления сущности по "ОК"
|
||||||
|
const handleEntityAddDialogOk = async values => {
|
||||||
|
await addEnt(values.name, "VIEW");
|
||||||
|
setOpenEntityAddDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Закрытие диалога добавления сущности по "ОК"
|
||||||
|
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
|
||||||
|
|
||||||
//Панель инструмментов
|
//Панель инструмментов
|
||||||
const toolBar = (
|
const toolBar = (
|
||||||
<P8PEditorToolBar
|
<P8PEditorToolBar
|
||||||
@ -154,34 +101,28 @@ const QueryEditor = () => {
|
|||||||
return (
|
return (
|
||||||
<Box sx={STYLES.CONTAINER}>
|
<Box sx={STYLES.CONTAINER}>
|
||||||
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
|
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
|
||||||
|
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
|
||||||
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
|
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
|
||||||
<Grid item xs={20}>
|
<Grid item xs={20}>
|
||||||
{queryDiagram && (
|
{queryDiagram && (
|
||||||
<QueryDiagram
|
<QueryDiagram
|
||||||
{...queryDiagram}
|
{...queryDiagram}
|
||||||
onEntityClick={handleEntityClick}
|
|
||||||
onEntityAttrClick={handleEntityAttrClick}
|
|
||||||
onEntityPositionChange={handleEntityPositionChange}
|
onEntityPositionChange={handleEntityPositionChange}
|
||||||
onEntityRemove={handleEntityRemove}
|
onEntityRemove={handleEntityRemove}
|
||||||
onRelactionClick={handleRelationClick}
|
|
||||||
onRelationAdd={handleRelationAdd}
|
onRelationAdd={handleRelationAdd}
|
||||||
onRelationRemove={handleRelationRemove}
|
onRelationRemove={handleRelationRemove}
|
||||||
onPaneClick={handlePaneClick}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
|
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
|
||||||
{toolBar}
|
{toolBar}
|
||||||
{query && (
|
{query && (
|
||||||
<QueryOptions
|
<P8PEditorBox title={"Параметры запроса"}>
|
||||||
query={query}
|
<P8PEditorSubHeader title={"Сущности"} />
|
||||||
entity={entity}
|
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAdd}>
|
||||||
relation={relation}
|
{BUTTONS.INSERT}
|
||||||
onEntityAdd={handleEntityAdd}
|
</Button>
|
||||||
onEntityRemove={handleEntityRemove}
|
</P8PEditorBox>
|
||||||
onRelationRemove={handleRelationRemove}
|
|
||||||
onQueryOptionsChanged={handleQueryOptionsChanged}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
2705
db/PKG_P8PANELS_CLNTTSKBRD.pck
Normal file
2705
db/PKG_P8PANELS_CLNTTSKBRD.pck
Normal file
File diff suppressed because it is too large
Load Diff
@ -59,22 +59,6 @@ create or replace package PKG_P8PANELS_QE as
|
|||||||
NY in number -- Координата по оси ординат
|
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
|
procedure QUERY_RL_ADD
|
||||||
(
|
(
|
||||||
@ -208,6 +192,7 @@ create or replace package body PKG_P8PANELS_QE as
|
|||||||
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
|
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
|
||||||
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
|
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
|
||||||
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
|
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
|
||||||
|
RRLS_TMP PKG_P8PANELS_QE_BASE.TRLS; -- Буфер для коллекции удаляемых связей
|
||||||
begin
|
begin
|
||||||
/* Провим права доступа */
|
/* Провим права доступа */
|
||||||
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
|
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
|
||||||
@ -233,8 +218,20 @@ create or replace package body PKG_P8PANELS_QE as
|
|||||||
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
|
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
|
||||||
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
|
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
|
||||||
loop
|
loop
|
||||||
/* Удаляем связи в которых он задействован */
|
/* Если атрибут есть в связях (как источник или как приёмник) */
|
||||||
PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RENT.RATTRS(I).SID);
|
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;
|
||||||
end loop;
|
end loop;
|
||||||
end if;
|
end if;
|
||||||
/* Сохраняем обновленный набор сущностей */
|
/* Сохраняем обновленный набор сущностей */
|
||||||
@ -268,77 +265,6 @@ create or replace package body PKG_P8PANELS_QE as
|
|||||||
end if;
|
end if;
|
||||||
end QUERY_ENT_POSITION_SET;
|
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
|
procedure QUERY_RL_ADD
|
||||||
(
|
(
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
create or replace package PKG_P8PANELS_QE_BASE as
|
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
|
type TATTR is record
|
||||||
(
|
(
|
||||||
SID PKG_STD.TSTRING, -- Уникальный идентификатор в запросе
|
SID PKG_STD.TSTRING, -- Уникальный идентификатор в запросе
|
||||||
SNAME PKG_STD.TSTRING, -- Имя
|
SNAME PKG_STD.TSTRING, -- Имя
|
||||||
STITLE 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,14 +42,6 @@ create or replace package PKG_P8PANELS_QE_BASE as
|
|||||||
/* Типы данных - Коллекция отношений */
|
/* Типы данных - Коллекция отношений */
|
||||||
type TRLS is table of TRL;
|
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
|
function TENTS_INDEX_BY_ID
|
||||||
(
|
(
|
||||||
@ -81,6 +73,14 @@ create or replace package PKG_P8PANELS_QE_BASE as
|
|||||||
NY in number -- Координата по оси ординат
|
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
|
procedure TRLS_APPEND
|
||||||
(
|
(
|
||||||
@ -96,13 +96,6 @@ create or replace package PKG_P8PANELS_QE_BASE as
|
|||||||
SID in varchar2 -- Идентификатор удялемой связи
|
SID in varchar2 -- Идентификатор удялемой связи
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Подчистка коллекции связей по атрибуту */
|
|
||||||
procedure TRLS_CLEANUP_BY_ATTR
|
|
||||||
(
|
|
||||||
RRLS in out nocopy TRLS, -- Изменяемая коллекция
|
|
||||||
SATTR_ID in varchar2 -- Идентификатор атрибута
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Считывание записи запроса */
|
/* Считывание записи запроса */
|
||||||
function QUERY_GET
|
function QUERY_GET
|
||||||
(
|
(
|
||||||
@ -184,14 +177,6 @@ create or replace package PKG_P8PANELS_QE_BASE as
|
|||||||
RENTS in TENTS -- Коллекция сущностей
|
RENTS in TENTS -- Коллекция сущностей
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Получение состава атрибутов сущности */
|
|
||||||
procedure QUERY_ENT_ATTRS_GET
|
|
||||||
(
|
|
||||||
NRN in number, -- Рег. номер запроса
|
|
||||||
SID in varchar2, -- Идентификатор сущности
|
|
||||||
COUT out clob -- Сериализованное описание атрибутов сущности
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Чтение связей запроса */
|
/* Чтение связей запроса */
|
||||||
function QUERY_RLS_GET
|
function QUERY_RLS_GET
|
||||||
(
|
(
|
||||||
@ -223,10 +208,6 @@ end PKG_P8PANELS_QE_BASE;
|
|||||||
/
|
/
|
||||||
create or replace package body PKG_P8PANELS_QE_BASE as
|
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_DATA constant PKG_STD.TSTRING := 'XDATA'; -- Данные
|
||||||
STAG_QUERIES constant PKG_STD.TSTRING := 'XQUERIES'; -- Запросы
|
STAG_QUERIES constant PKG_STD.TSTRING := 'XQUERIES'; -- Запросы
|
||||||
@ -257,10 +238,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
SATTR_Y constant PKG_STD.TSTRING := 'y'; -- Координата по Y
|
SATTR_Y constant PKG_STD.TSTRING := 'y'; -- Координата по Y
|
||||||
SATTR_SOURCE constant PKG_STD.TSTRING := 'source'; -- Источник
|
SATTR_SOURCE constant PKG_STD.TSTRING := 'source'; -- Источник
|
||||||
SATTR_TARGET constant PKG_STD.TSTRING := 'target'; -- Приёмник
|
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
|
function DMSCLVIEWS_TITLE_GET
|
||||||
@ -351,10 +328,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RATTR.SNAME);
|
PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RATTR.SNAME);
|
||||||
PKG_XFAST.ATTR(SNAME => SATTR_TITLE, SVALUE => RATTR.STITLE);
|
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_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();
|
PKG_XFAST.UP();
|
||||||
end TATTR_TO_XML;
|
end TATTR_TO_XML;
|
||||||
@ -383,10 +356,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
RRES.SNAME := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_NAME);
|
RRES.SNAME := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_NAME);
|
||||||
RRES.STITLE := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_TITLE);
|
RRES.STITLE := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_TITLE);
|
||||||
RRES.NDATA_TYPE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_DATA_TYPE);
|
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);
|
PKG_XPATH.FREE(RDOCUMENT => XDOC);
|
||||||
exception
|
exception
|
||||||
@ -402,119 +371,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
return RRES;
|
return RRES;
|
||||||
end TATTR_FROM_XML;
|
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
|
procedure TATTRS_TO_XML
|
||||||
(
|
(
|
||||||
@ -583,26 +439,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
return RRES;
|
return RRES;
|
||||||
end TATTRS_FROM_XML;
|
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
|
function TENT_ID_MAKE
|
||||||
(
|
(
|
||||||
@ -633,6 +469,8 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
is
|
is
|
||||||
RENT TENT; -- Буфер для результата
|
RENT TENT; -- Буфер для результата
|
||||||
RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления
|
RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления
|
||||||
|
RVIEW_FIELDS PKG_OBJECT_DESC.TCOLUMNS; -- Коллекция описаний полей представления
|
||||||
|
RVIEW_FIELD PKG_OBJECT_DESC.TCOLUMN; -- Описание поля представления
|
||||||
begin
|
begin
|
||||||
/* Проверим корректность типа сущности */
|
/* Проверим корректность типа сущности */
|
||||||
if (STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
|
if (STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
|
||||||
@ -644,13 +482,30 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
if (STYPE = SENT_TYPE_VIEW) then
|
if (STYPE = SENT_TYPE_VIEW) then
|
||||||
/* Получим описание представления */
|
/* Получим описание представления */
|
||||||
RVIEW := PKG_OBJECT_DESC.DESC_VIEW(SVIEW_NAME => SNAME, BRAISE_ERROR => true);
|
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.SID := TENT_ID_MAKE(SNAME => RVIEW.VIEW_NAME, NNUMB => NNUMB);
|
||||||
RENT.SNAME := RVIEW.VIEW_NAME;
|
RENT.SNAME := RVIEW.VIEW_NAME;
|
||||||
RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME);
|
RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME);
|
||||||
RENT.STYPE := SENT_TYPE_VIEW;
|
RENT.STYPE := SENT_TYPE_VIEW;
|
||||||
/* Формируем набор атрибутов */
|
RENT.RATTRS := TATTRS();
|
||||||
RENT.RATTRS := TATTRS_MAKE(RENT => RENT, NCOUNT => 10);
|
/* Собираем атрибуты в ответ */
|
||||||
|
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;
|
||||||
end if;
|
end if;
|
||||||
/* Если сущность это таблица */
|
/* Если сущность это таблица */
|
||||||
if (STYPE = SENT_TYPE_TABLE) then
|
if (STYPE = SENT_TYPE_TABLE) then
|
||||||
@ -1090,29 +945,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
end if;
|
end if;
|
||||||
end TRLS_REMOVE;
|
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
|
procedure TRLS_TO_XML
|
||||||
(
|
(
|
||||||
@ -1652,56 +1484,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as
|
|||||||
QUERY_CH_DATE_SYNC(NRN => NRN);
|
QUERY_CH_DATE_SYNC(NRN => NRN);
|
||||||
end QUERY_ENTS_SET;
|
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
|
function QUERY_RLS_GET
|
||||||
(
|
(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user