forked from CITKParus/P8-Panels
317 lines
16 KiB
JavaScript
317 lines
16 KiB
JavaScript
/*
|
||
Парус 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 };
|