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 };
 |