272 lines
12 KiB
JavaScript
272 lines
12 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, useTasks, useSettings } from "./hooks/hooks.js"; //Вспомогательные хуки
|
||
import { useFilters } from "./hooks/filter_hooks.js";
|
||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||
import { NoteDialog } from "./components/note_dialog.js"; //Диалог примечания
|
||
import { SettingsDialog } from "./components/settings_dialog.js"; //Диалог дополнительных настроек
|
||
import { deepCopyObject } from "../../core/utils.js"; //Вспомогательные функции
|
||
|
||
//---------
|
||
//Константы
|
||
//---------
|
||
|
||
//Высота заголовка
|
||
const TITLE_HEIGHT = "64px";
|
||
|
||
//Нижний отступ заголовка
|
||
const TITLE_PADDING_BOTTOM = "16px";
|
||
|
||
//Высота фильтра
|
||
const FILTER_HEIGHT = "56px";
|
||
|
||
//Стили
|
||
const STYLES = {
|
||
CONTAINER: { width: "100%", padding: 0 },
|
||
FS_BOX: { display: "flex", alignItems: "center" },
|
||
SETTINGS_MARGIN: { marginLeft: "auto" },
|
||
STATUSES_STACK: { maxWidth: "99vw", paddingBottom: "5px", overflowX: "auto", ...APP_STYLES.SCROLL },
|
||
STATUS_BLOCK: statusColor => {
|
||
return {
|
||
width: "350px",
|
||
height: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`,
|
||
backgroundColor: statusColor,
|
||
padding: "8px"
|
||
};
|
||
},
|
||
BLOCK_OPACITY: isAvailable => {
|
||
return isAvailable ? { opacity: 1 } : { opacity: 0.5 };
|
||
},
|
||
STATUSES_DIV: { position: "fixed", left: "8px", top: `calc(${TITLE_HEIGHT} + ${FILTER_HEIGHT})` },
|
||
CARD_CONTENT: {
|
||
padding: 0,
|
||
paddingRight: "5px",
|
||
paddingBottom: "5px !important",
|
||
overflowY: "auto",
|
||
maxHeight: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 85px)`,
|
||
...APP_STYLES.SCROLL
|
||
},
|
||
MARK_INFO: {
|
||
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"
|
||
},
|
||
PADDING_0: { padding: 0 }
|
||
};
|
||
|
||
//-----------
|
||
//Тело модуля
|
||
//-----------
|
||
|
||
//Корневая панель доски задач
|
||
const ClntTaskBoard = () => {
|
||
//Состояние вспомогательных диалогов
|
||
const [dialogsState, setDialogsState] = useState({
|
||
filterOpen: false,
|
||
settingsOpen: false,
|
||
note: { isOpen: false, callback: null },
|
||
taskDialogOpen: false
|
||
});
|
||
|
||
//Открыть-закрыть диалог фильтра
|
||
const handleFilterOpen = isOpen => {
|
||
setDialogsState(pv => ({ ...pv, filterOpen: isOpen }));
|
||
};
|
||
|
||
//Открыть-закрыть диалог дополнительных настроек
|
||
const handleSettingsOpen = () => setDialogsState(pv => ({ ...pv, settingsOpen: !dialogsState.settingsOpen }));
|
||
|
||
//Открыть-закрыть диалог примечания
|
||
const handleNoteOpen = (f = null) => setDialogsState(pv => ({ ...pv, note: { isOpen: !dialogsState.noteOpen, callback: f ? v => f(v) : null } }));
|
||
|
||
//Открыть-закрыть диалог события
|
||
const handleTaskDialogOpen = () => setDialogsState(pv => ({ ...pv, taskDialogOpen: !dialogsState.taskDialogOpen }));
|
||
|
||
//Состояние фильтров
|
||
const [filters, handleFiltersChange] = useFilters(handleFilterOpen);
|
||
|
||
//Состояние сортировок
|
||
const [orders, setOrders] = useState([]);
|
||
|
||
//Состояние дополнительных данных
|
||
const [extraData, getDocLinks, needUpdateExtraData] = useExtraData(filters.values.type);
|
||
|
||
//Состояние событий
|
||
const [tasks, handleReload, onDragEnd, needUpdateTasks] = useTasks({ filters, orders, extraData, getDocLinks });
|
||
|
||
//Состояние дополнительных настроек
|
||
const [settings, handleSettingsChange] = useSettings(tasks.statuses);
|
||
|
||
//Состояние доступных маршрутов события
|
||
const [availableRoutes, setAvailableRoutes] = useState({ sorce: "", routes: [] });
|
||
|
||
//При изменении сортировки
|
||
const handleOrderChanged = useCallback(
|
||
columnName => {
|
||
let newOrders = deepCopyObject(orders);
|
||
const colOrder = newOrders.find(o => o.name == columnName);
|
||
const newDirection = colOrder?.direction == "ASC" ? "DESC" : colOrder?.direction == "DESC" ? null : "ASC";
|
||
if (newDirection == null && colOrder) newOrders.splice(newOrders.indexOf(colOrder), 1);
|
||
if (newDirection != null && !colOrder) newOrders.push({ name: columnName, direction: newDirection });
|
||
if (newDirection != null && colOrder) colOrder.direction = newDirection;
|
||
setOrders(newOrders);
|
||
},
|
||
[orders]
|
||
);
|
||
|
||
//Очистка состояния доступных маршрутов события
|
||
const clearAvailableRoutesState = () => {
|
||
setAvailableRoutes({ sorce: "", routes: [] });
|
||
};
|
||
|
||
//Состояние перетаскиваемого события
|
||
const [dragItem, setDragItem] = useState({ type: "", status: "" });
|
||
|
||
//Захватить перетаскиваемый объект
|
||
const handleDragItemChange = (filtersType, statusCode) =>
|
||
setDragItem({
|
||
type: filtersType,
|
||
status: statusCode
|
||
});
|
||
|
||
//Отпустить перетаскиваемый объект
|
||
const handleDragItemClear = () => {
|
||
setDragItem({ type: "", status: "" });
|
||
};
|
||
|
||
//Проверка доступности карточки события
|
||
const isCardAvailable = code => {
|
||
return availableRoutes.sorce === code || availableRoutes.routes.find(r => r.dest === code) || !availableRoutes.sorce ? true : false;
|
||
};
|
||
|
||
//При смене типа события
|
||
useEffect(() => {
|
||
if (filters.values.type) {
|
||
//Обновление вспомогательных данных
|
||
filters.values.type !== extraData.typeLoaded ? needUpdateExtraData() : null;
|
||
//Обновление событий
|
||
needUpdateTasks();
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [filters.values.type]);
|
||
|
||
//Генерация содержимого
|
||
return (
|
||
<Box sx={STYLES.CONTAINER}>
|
||
{dialogsState.settingsOpen ? (
|
||
<SettingsDialog initial={settings} onSettingsChange={handleSettingsChange} onOpen={handleSettingsOpen} />
|
||
) : null}
|
||
{dialogsState.taskDialogOpen ? (
|
||
<TaskDialog
|
||
taskType={dragItem.type}
|
||
taskStatus={dragItem.status}
|
||
onReload={handleReload}
|
||
onClose={() => {
|
||
handleTaskDialogOpen();
|
||
handleDragItemClear();
|
||
}}
|
||
/>
|
||
) : null}
|
||
<Box sx={STYLES.FS_BOX}>
|
||
<Stack direction="row">
|
||
<Filter
|
||
isFilterDialogOpen={dialogsState.filterOpen}
|
||
filter={filters.values}
|
||
docs={extraData.docLinks}
|
||
selectedDoc={filters.values.docLink ? extraData.docLinks.find(d => d.id === filters.values.docLink) : null}
|
||
onFilterChange={handleFiltersChange}
|
||
getDocLinks={getDocLinks}
|
||
onFilterOpen={() => handleFilterOpen(true)}
|
||
onFilterClose={() => handleFilterOpen(false)}
|
||
onReload={handleReload}
|
||
orders={orders}
|
||
onOrderChanged={handleOrderChanged}
|
||
/>
|
||
</Stack>
|
||
<IconButton title="Настройки" onClick={handleSettingsOpen} sx={STYLES.SETTINGS_MARGIN}>
|
||
<Icon>settings</Icon>
|
||
</IconButton>
|
||
</Box>
|
||
{dialogsState.note.isOpen ? (
|
||
<NoteDialog noteTypes={extraData.noteTypes} onCallback={n => dialogsState.note.callback(n)} onNoteOpen={handleNoteOpen} />
|
||
) : null}
|
||
{filters.loaded && filters.values.type && extraData.dataLoaded && tasks.groupsLoaded && tasks.tasksLoaded ? (
|
||
<DragDropContext
|
||
onDragStart={e => {
|
||
let srcCode = tasks.statuses.find(s => s.id == e.source.droppableId).code;
|
||
setAvailableRoutes({ sorce: srcCode, routes: [...extraData.evRoutes.filter(r => r.src === srcCode)] });
|
||
}}
|
||
onDragEnd={e => {
|
||
onDragEnd(e, extraData.evPoints, handleNoteOpen);
|
||
clearAvailableRoutesState();
|
||
}}
|
||
>
|
||
<div style={STYLES.STATUSES_DIV}>
|
||
<Droppable droppableId="Statuses" type="droppableTask">
|
||
{provided => (
|
||
<div ref={provided.innerRef}>
|
||
<Stack direction="row" spacing={2} sx={STYLES.STATUSES_STACK}>
|
||
{settings.statusesSort.sorted
|
||
? settings.statusesSort.statuses.map((status, index) => (
|
||
<div key={index}>
|
||
<Droppable isDropDisabled={!isCardAvailable(status.code)} droppableId={status.id.toString()}>
|
||
{provided => (
|
||
<div ref={provided.innerRef}>
|
||
<StatusCard
|
||
tasks={tasks}
|
||
status={status}
|
||
settings={settings}
|
||
extraData={extraData}
|
||
filtersType={filters.values.type}
|
||
isCardAvailable={isCardAvailable}
|
||
onReload={handleReload}
|
||
onDragItemChange={handleDragItemChange}
|
||
onTaskDialogOpen={handleTaskDialogOpen}
|
||
onNoteDialogOpen={handleNoteOpen}
|
||
placeholder={provided.placeholder}
|
||
/>
|
||
</div>
|
||
)}
|
||
</Droppable>
|
||
</div>
|
||
))
|
||
: null}
|
||
</Stack>
|
||
{provided.placeholder}
|
||
</div>
|
||
)}
|
||
</Droppable>
|
||
</div>
|
||
</DragDropContext>
|
||
) : null}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
//----------------
|
||
//Интерфейс модуля
|
||
//----------------
|
||
|
||
export { ClntTaskBoard };
|