From 4b2d589e632bc81135540ca07670f86bb56114bc Mon Sep 17 00:00:00 2001 From: davay-popozhe Date: Thu, 20 Feb 2025 16:11:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A6=D0=98=D0=A2=D0=9A-932;=D0=A6=D0=98=D0=A2?= =?UTF-8?q?=D0=9A-933;=D0=A6=D0=98=D0=A2=D0=9A-935?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/panels/clnt_task_board/clnt_task_board.js | 221 +++++---- .../components/filter_dialog.js | 33 +- .../components/filter_input_field.js | 6 +- .../clnt_task_board/components/note_dialog.js | 5 +- .../components/rules_select.js | 93 ++++ .../components/settings_dialog.js | 140 ++++++ .../clnt_task_board/components/task_card.js | 89 +++- .../components/task_card_settings.js | 10 +- .../clnt_task_board/components/task_form.js | 258 ++++++++++- app/panels/clnt_task_board/hooks.js | 436 +++++++++++++----- 10 files changed, 1019 insertions(+), 272 deletions(-) create mode 100644 app/panels/clnt_task_board/components/rules_select.js create mode 100644 app/panels/clnt_task_board/components/settings_dialog.js diff --git a/app/panels/clnt_task_board/clnt_task_board.js b/app/panels/clnt_task_board/clnt_task_board.js index 1cf1d9e..48ae00e 100644 --- a/app/panels/clnt_task_board/clnt_task_board.js +++ b/app/panels/clnt_task_board/clnt_task_board.js @@ -15,9 +15,10 @@ import { TaskFormDialog } from "./components/task_form"; //Компонент ф import { Filter } from "./filter.js"; //Компонент фильтров import { FilterDialog } from "./components/filter_dialog"; //Компонент диалогового окна фильтра отбора import { TaskCardSettings } from "./components/task_card_settings.js"; //Компонент настроек карточки события -import { useTasks, COLORS } from "./hooks.js"; //Вспомогательные хуки +import { useTasks, useSettings, COLORS } from "./hooks.js"; //Вспомогательные хуки import { APP_STYLES } from "../../../app.styles"; //Типовые стили import { NoteDialog } from "./components/note_dialog.js"; //Диалог примечания +import { SettingsDialog } from "./components/settings_dialog.js"; //Диалог дополнительных настроек //--------- //Константы @@ -35,11 +36,10 @@ const FILTER_HEIGHT = "56px"; //Стили const STYLES = { CONTAINER: { width: "100%", padding: 0 }, - FILTER: { position: "fixed" }, STATUSES_STACK: { maxWidth: "99vw", paddingBottom: "5px", overflowX: "auto", ...APP_STYLES.SCROLL }, STATUS_BLOCK: statusColor => { return { - width: "315px", + width: "350px", height: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`, backgroundColor: statusColor, padding: "8px" @@ -61,9 +61,14 @@ const STYLES = { textAlign: "left", textOverflow: "ellipsis", overflow: "hidden", - whiteSpace: "pre", - maxWidth: "calc(250px)", - width: "-webkit-fill-available" + 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 } }; @@ -77,11 +82,6 @@ const ClntTaskBoard = () => { //Собственное состояние const [ tasks, - eventRoutes, - eventPoints, - noteTypes, - docLinks, - accounts, taskFormOpen, setTaskFormOpen, cardSettings, @@ -97,6 +97,9 @@ const ClntTaskBoard = () => { getDocLinks ] = useTasks(); + //Состояние дополнительных настроек + const [settings, settingsOpen, settingsClose, handleSettingsOk] = useSettings(tasks.statuses); + //Состояние диалога примечания const [noteDialog, setNoteDialog] = useState({ visible: false, callback: null }); @@ -109,6 +112,11 @@ const ClntTaskBoard = () => { //Состояние доступных маршрутов события const [availableRoutes, setAvailableRoutes] = useState({ sorce: "", routes: [] }); + //Очистка состояния доступных маршрутов события + const clearAvailableRoutesState = () => { + setAvailableRoutes({ sorce: "", routes: [] }); + }; + //Состояние перетаскиваемого события const [dragItem, setDragItem] = useState({ type: "", status: "" }); @@ -117,11 +125,6 @@ const ClntTaskBoard = () => { setDragItem({ type: "", status: "" }); }; - //Очистка состояния - const clearARState = () => { - setAvailableRoutes({ sorce: "", routes: [] }); - }; - //Проверка доступности карточки события const isCardAvailable = code => { return availableRoutes.sorce === code || availableRoutes.routes.find(r => r.dest === code) || !availableRoutes.sorce ? true : false; @@ -133,31 +136,41 @@ const ClntTaskBoard = () => { {tasks.filters.isOpen ? ( ) : null} - d.id === tasks.filters.values.docLink) : null} - handleFilterClick={handleFilterClick} - handleReload={handleReload} - orders={tasks.orders} - handleOrderChanged={handleOrderChanged} - sx={STYLES.FILTER} - /> - {noteDialog.visible ? noteDialog.callback(n)} onCancel={handleNoteDialogClose} /> : null} + {settings.open ? : null} + + + d.id === tasks.filters.values.docLink) : null} + handleFilterClick={handleFilterClick} + handleReload={handleReload} + orders={tasks.orders} + handleOrderChanged={handleOrderChanged} + //sx={STYLES.FILTER} + /> + + + settings + + + {noteDialog.visible ? ( + noteDialog.callback(n)} onCancel={handleNoteDialogClose} /> + ) : null} {tasks.filters.values.type ? ( { let srcCode = tasks.statuses.find(s => s.id == e.source.droppableId).code; - setAvailableRoutes({ sorce: srcCode, routes: [...eventRoutes.filter(r => r.src === srcCode)] }); + setAvailableRoutes({ sorce: srcCode, routes: [...tasks.extraData.evRoutes.filter(r => r.src === srcCode)] }); }} onDragEnd={e => { - onDragEnd(e, eventPoints, handleNoteDialogOpen); - clearARState(); + onDragEnd(e, tasks.extraData.evPoints, handleNoteDialogOpen); + clearAvailableRoutesState(); }} >
@@ -165,74 +178,85 @@ const ClntTaskBoard = () => { {provided => (
- {tasks.statuses.map((status, index) => ( -
- - {provided => ( -
- - handleCardSettingsClick(status)} - > - more_vert - - } - title={ - - {status.caption} - - } - subheader={ - - } - sx={STYLES.PADDING_0} - /> - - - {tasks.rows - .filter(item => item.category === status.id) - .map((item, index) => ( - - a.evRnList.find(rn => rn == item.nrn) - )} - index={index} - handleReload={handleReload} - key={item.id} - eventPoints={eventPoints} - pointSettings={eventPoints.find(p => p.point === status.code)} - openNoteDialog={handleNoteDialogOpen} - /> - ))} - {provided.placeholder} - - - -
- )} -
-
- ))} + {settings.statusesSort.sorted + ? settings.statusesSort.statuses.map((status, index) => ( +
+ + {provided => ( +
+ + handleCardSettingsClick(status)} + > + more_vert + + } + title={ + + {status[settings.statusesSort.attr] || status.name} + + } + subheader={ + + } + sx={STYLES.PADDING_0} + /> + + + {tasks.rows + .filter(item => item.category === status.id) + .map((item, index) => ( + a.agnAbbr === item.sSender + ).image + } + index={index} + handleReload={handleReload} + key={item.id} + eventPoints={tasks.extraData.evPoints} + colorRule={settings.colorRule} + pointSettings={tasks.extraData.evPoints.find( + p => p.point === status.code + )} + openNoteDialog={handleNoteDialogOpen} + /> + ))} + {provided.placeholder} + + + +
+ )} +
+
+ )) + : null}
{provided.placeholder}
@@ -245,6 +269,7 @@ const ClntTaskBoard = () => { { setTaskFormOpen(false); clearDragItem(); diff --git a/app/panels/clnt_task_board/components/filter_dialog.js b/app/panels/clnt_task_board/components/filter_dialog.js index 4814990..68197fe 100644 --- a/app/panels/clnt_task_board/components/filter_dialog.js +++ b/app/panels/clnt_task_board/components/filter_dialog.js @@ -34,6 +34,7 @@ import { APP_STYLES } from "../../../../app.styles"; //Типовые стили //Константы //--------- +//Перечисление "Состояние события" export const EVENT_STATES = Object.freeze({ 0: "Все", 1: "Не аннулированные", 2: "Аннулированные" }); //Стили @@ -101,7 +102,6 @@ const selectSendDivision = (value, showDictionary, callBack) => { const selectSendUsrGrp = (value, showDictionary, callBack) => { showDictionary({ unitCode: "CostStaffGroups", - //showMethod: "dictionary", inputParameters: [{ name: "in_CODE", value: value }], callBack: res => (res.success === true ? callBack(res.outParameters.out_CODE) : callBack(null)) }); @@ -120,15 +120,12 @@ const FilterDialog = ({ initial, docs, onCancel, onOk, getDocLinks }) => { const [curType, setCurType] = useState(initial.type); //Состояние учётных документов - const [curDocLinks, setCurDocLinks] = useState([...docs]); - - //Состояние изменения типа события - const [typeDif, setTypeDif] = useState(false); + const [curDocLinks, setCurDocLinks] = useState(docs); //Подключение к контексту взаимодействия с сервером const { executeStored } = useContext(BackEndСtx); - //Получение субкаталогов + //Получение подкаталогов const getSubCatalogs = useCallback(async () => { const data = await executeStored({ stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SUBCATALOGS_GET", @@ -174,7 +171,9 @@ const FilterDialog = ({ initial, docs, onCancel, onOk, getDocLinks }) => { }; //При изменении значения элемента - const handleFilterItemChange = (item, value) => setFilter(pv => ({ ...pv, [item]: value })); + const handleFilterItemChange = (item, value) => { + item === "type" && filter.docLink ? setFilter(pv => ({ ...pv, [item]: value, docLink: "" })) : setFilter(pv => ({ ...pv, [item]: value })); + }; //Очистка учётного документа const clearDocLink = () => setFilter(pv => ({ ...pv, docLink: "" })); @@ -182,13 +181,12 @@ const FilterDialog = ({ initial, docs, onCancel, onOk, getDocLinks }) => { //При изменении типа события useEffect(() => { if (curType) { - if (curType === filter.type) setTypeDif(false); - else { - setTypeDif(true); + if (curType !== filter.type) { clearDocLink(); - } + setCurDocLinks([]); + } else if (curType === filter.type && curType === initial.type && !curDocLinks.length) setCurDocLinks(docs); } - }, [curType, filter.type]); + }, [curDocLinks, curType, docs, filter.type, initial.type]); //Обработка изменений с каталогами useEffect(() => { @@ -287,10 +285,10 @@ const FilterDialog = ({ initial, docs, onCancel, onOk, getDocLinks }) => { @@ -299,12 +297,13 @@ const FilterDialog = ({ initial, docs, onCancel, onOk, getDocLinks }) => { { setCurType(filter.type); clearDocLink(); getDocLinks(filter.type).then(dl => { - setCurDocLinks([...dl]); + setCurDocLinks(dl); + console.log(dl); }); }} > diff --git a/app/panels/clnt_task_board/components/filter_input_field.js b/app/panels/clnt_task_board/components/filter_input_field.js index 187aaf7..8c5bdba 100644 --- a/app/panels/clnt_task_board/components/filter_input_field.js +++ b/app/panels/clnt_task_board/components/filter_input_field.js @@ -19,7 +19,9 @@ import { APP_STYLES } from "../../../../app.styles"; //Типовые стили //Стили const STYLES = { HELPER_TEXT: { color: "red" }, - SELECT_MENU: { overflowY: "auto", ...APP_STYLES.SCROLL } + SELECT_MENU: w => { + return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null }; + } }; //--------------- @@ -82,7 +84,7 @@ const FilterInputField = ({ elementCode, elementValue, labelText, onChange, requ value={value} aria-describedby={`${elementCode}-helper-text`} label={labelText} - MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU } } }} + MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU(document.getElementById(elementCode)?.parentElement.clientWidth) } } }} onChange={handleChange} {...other} > diff --git a/app/panels/clnt_task_board/components/note_dialog.js b/app/panels/clnt_task_board/components/note_dialog.js index 57f1ab6..74a2bc6 100644 --- a/app/panels/clnt_task_board/components/note_dialog.js +++ b/app/panels/clnt_task_board/components/note_dialog.js @@ -1,6 +1,6 @@ /* Парус 8 - Панели мониторинга - УДП - Доски задач - Компонент: Диалоговое окно примечания + Компонент: Диалог примечания */ //--------------------- @@ -46,6 +46,7 @@ const STYLES = { //Тело компонента //--------------- +//Диалог примечания const NoteDialog = ({ noteTypes, onOk, onCancel }) => { //Собственное состояние const [note, setNote] = useState({ headerV: 0, text: "" }); @@ -125,7 +126,7 @@ const NoteDialog = ({ noteTypes, onOk, onCancel }) => { ); }; -//Контроль свойств - Диалоговое окно примечания +//Контроль свойств - Диалог примечания NoteDialog.propTypes = { noteTypes: PropTypes.array, onOk: PropTypes.func.isRequired, diff --git a/app/panels/clnt_task_board/components/rules_select.js b/app/panels/clnt_task_board/components/rules_select.js new file mode 100644 index 0000000..35eee62 --- /dev/null +++ b/app/panels/clnt_task_board/components/rules_select.js @@ -0,0 +1,93 @@ +/* + Парус 8 - Панели мониторинга - УДП - Доски задач + Компонент: Выпадающий список выбора заливки событий +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useEffect, useState } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; //Интерфейсные компоненты +import { useColorRules } from "../hooks.js"; //Вспомогательные хуки +import { APP_STYLES } from "../../../../app.styles"; //Типовые стили + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + SELECT_MENU: w => { + return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null }; + } +}; + +//--------------- +//Тело компонента +//--------------- + +//Выпадающий список выбора заливки событий +const RulesSelect = ({ initRule, handleChange, ...other }) => { + //Состояние пользовательских настроек заливки событий + const [colorRules] = useColorRules(); + + //Собственное состояние + const [curRule, setCurRule] = useState(initRule > -1 ? "" : initRule); + + //При получении нового значения заливки из вне + useEffect(() => { + if ( + (colorRules.loaded && initRule > -1 && curRule === "") || + (Number.isInteger(initRule) && Number.isInteger(curRule) && initRule !== curRule) + ) + setCurRule(initRule); + }, [colorRules, curRule, initRule]); + + //При изменении заливки событий + const handleRuleChange = e => { + let id = e.target.value; + setCurRule(id); + handleChange(id > -1 ? colorRules.rules[id] : {}); + }; + + //Генерация содержимого + return colorRules ? ( + + Заливка событий + + + ) : null; +}; + +//Контроль свойств - Выпадающий список выбора заливки событий +RulesSelect.propTypes = { + initRule: PropTypes.number.isRequired, + handleChange: PropTypes.func.isRequired +}; + +//-------------------- +//Интерфейс компонента +//-------------------- + +export { RulesSelect }; diff --git a/app/panels/clnt_task_board/components/settings_dialog.js b/app/panels/clnt_task_board/components/settings_dialog.js new file mode 100644 index 0000000..8f826df --- /dev/null +++ b/app/panels/clnt_task_board/components/settings_dialog.js @@ -0,0 +1,140 @@ +/* + Парус 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 { RulesSelect } from "./rules_select.js"; +import { FilterInputField } from "./filter_input_field.js"; +import { APP_STYLES } from "../../../../app.styles"; //Типовые стили + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + FILTERS_SCROLL: { overflowY: "auto", ...APP_STYLES.SCROLL }, + DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" }, + CLOSE_BUTTON: { + position: "absolute", + right: 8, + top: 8, + color: theme => theme.palette.grey[500] + }, + DOCLINK_STACK: { alignItems: "baseline" }, + SELECT: { width: "100%" }, + BOX_WITH_LEGEND: { border: "1px solid #939393" }, + LEGEND: { textAlign: "left" }, + SELECT_MENU: w => { + return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null }; + } +}; + +//--------------- +//Тело компонента +//--------------- + +//Диалог дополнительных настроек +const SettingsDialog = ({ initial, onOk, onCancel, ...other }) => { + //Состояние дополнительных настроек + const [settings, setSettings] = useState( + initial.statusesSort.attr ? { ...initial } : { ...initial, statusesSort: { sorted: true, attr: "name", dest: "asc" } } + ); + + //Допустимые значение поля сортировки + const sortAttrs = [ + { id: "code", descr: "Мнемокод" }, + { id: "name", descr: "Наименование" }, + { id: "pointDescr", descr: "Описание точки маршрута" } + ]; + + //Допустимые значения направления сортировки + const sortDest = []; + sortDest[-1] = "desc"; + sortDest[1] = "asc"; + + //Изменение заливки событий + const handleColorRuleChange = cr => setSettings(pv => ({ ...pv, colorRule: cr })); + + //Изменение поля сортировки + const handleSortAttrChange = (item, value) => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, [item]: value } })); + + //Изменение направления сортировки + const handleSortDestChange = d => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, dest: d } })); + + //Генерация содержимого + return ( +
+ + Настройки + + close + + + + + + + + + handleSortDestChange(sortDest[sortDest.indexOf(settings.statusesSort.dest) * -1])} + > + {settings.statusesSort.dest === "asc" ? "arrow_upward" : "arrow_downward"} + + + + + + + + + + +
+ ); +}; + +//Контроль свойств компонента - Диалог дополнительных настроек +SettingsDialog.propTypes = { + initial: PropTypes.object.isRequired, + onOk: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +//-------------------- +//Интерфейс компонента +//-------------------- + +export { SettingsDialog }; diff --git a/app/panels/clnt_task_board/components/task_card.js b/app/panels/clnt_task_board/components/task_card.js index 388cba1..f742e7f 100644 --- a/app/panels/clnt_task_board/components/task_card.js +++ b/app/panels/clnt_task_board/components/task_card.js @@ -1,30 +1,39 @@ /* Парус 8 - Панели мониторинга - УДП - Доски задач - Компонент панели: Карточка задачи + Компонент панели: Карточка события */ //--------------------- //Подключение библиотек //--------------------- -import React from "react"; //Классы React +import React, { useContext } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Draggable } from "react-beautiful-dnd"; import { Card, CardHeader, Typography, IconButton, Icon, Box, Menu, MenuItem, CardContent, Avatar, Stack } from "@mui/material"; //Интерфейсные компоненты import { useTaskCard } from "../hooks"; //Вспомогательные хуки import { TaskFormDialog } from "./task_form"; //Форма события +import { ApplicationСtx } from "../../../context/application"; //Контекст приложения //--------- //Константы //--------- +//Перечисление "Цвет индикации" +const EVENT_INDICATORS = Object.freeze({ EXPIRED: "#ff0000", EXPIRES_SOON: "#ffdf00", LINKED: "#1e90ff" }); + //Стили const STYLES = { CONTAINER: { margin: "5px 0px", textAlign: "center" }, 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: "252px", + width: "292px", display: "-webkit-box", hyphens: "auto", WebkitBoxOrient: "vertical", @@ -39,7 +48,9 @@ const STYLES = { color: "text.secondary", fontSize: 14 }, - ICON_COLOR: { color: theme => theme.palette.grey[500] } + ICON_COLOR: linked => { + return { color: theme => (linked ? EVENT_INDICATORS.LINKED : theme.palette.grey[500]) }; + } }; //------------------------------------ @@ -111,16 +122,60 @@ DataCellCardActions.propTypes = { //----------- //Карточка события -const TaskCard = ({ task, account, index, handleReload, eventPoints, pointSettings, openNoteDialog }) => { +const TaskCard = ({ task, avatar, index, handleReload, eventPoints, colorRule, pointSettings, openNoteDialog }) => { //Собственное состояние const [taskCard, setTaskCard, cardActions, handleMethodsMenuButtonClick, handleMethodsMenuClose, menuItems] = useTaskCard(); + //Подключение к контексту приложения + const { pOnlineShowDocument } = useContext(ApplicationСtx); + + //Конвертация формата HEX в формат RGB + const hexToRGB = 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 + ")"; + }; + + //Проверка выполнения условия заливки события + const bgColorRule = () => { + let ruleCode; + let bgColor = null; + if (colorRule.vType === "string") ruleCode = `S${colorRule.fieldCode}`; + else if (colorRule.vType === "number") ruleCode = `N${colorRule.fieldCode}`; + else if (colorRule.vType === "date") ruleCode = `D${colorRule.fieldCode}`; + ruleCode ? (task.docProps[ruleCode] == colorRule.from ? (bgColor = hexToRGB(colorRule.color)) : null) : null; + return bgColor; + }; + + //Индикация истечения срока отработки события + const indicatorColorRule = task => { + let sysDate = new Date(); + let expireDate = task.dexpire_date ? new Date(task.dexpire_date) : null; + let daysDiff = null; + if (expireDate) { + daysDiff = ((expireDate.getTime() - sysDate.getTime()) / (1000 * 60 * 60 * 24)).toFixed(2); + if (daysDiff < 0) return EVENT_INDICATORS.EXPIRED; + else if (daysDiff < 4) return EVENT_INDICATORS.EXPIRES_SOON; + } + return null; + }; + //Генерация содержимого return ( {provided => ( - + - assignment + pOnlineShowDocument({ unitCode: task.slinked_unit, document: task.nlinked_rn }) : null + } + sx={STYLES.ICON_COLOR(task.nlinked_rn)} + disabled={!task.nlinked_rn} + > + assignment + {task.name} - {account ? ( + {task.sSender ? ( - {account.authId ? account.authId : account.agnAbbr} - + {task.sSender} + ) : null} @@ -167,7 +231,9 @@ const TaskCard = ({ task, account, index, handleReload, eventPoints, pointSettin {taskCard.openEdit ? ( { setTaskCard(pv => ({ ...pv, openEdit: false })); }} @@ -180,10 +246,11 @@ const TaskCard = ({ task, account, index, handleReload, eventPoints, pointSettin //Контроль свойств - Карточка события TaskCard.propTypes = { task: PropTypes.object.isRequired, - account: PropTypes.object, + avatar: PropTypes.string, index: PropTypes.number.isRequired, handleReload: PropTypes.func, eventPoints: PropTypes.array, + colorRule: PropTypes.object, pointSettings: PropTypes.object, openNoteDialog: PropTypes.func }; diff --git a/app/panels/clnt_task_board/components/task_card_settings.js b/app/panels/clnt_task_board/components/task_card_settings.js index 6c58e04..8406dd0 100644 --- a/app/panels/clnt_task_board/components/task_card_settings.js +++ b/app/panels/clnt_task_board/components/task_card_settings.js @@ -1,6 +1,6 @@ /* Парус 8 - Панели мониторинга - УДП - Доски задач - Компонент: Диалоговое окно настройки карточки событий + Компонент: Диалог настройки карточки событий */ //--------------------- @@ -40,15 +40,11 @@ const STYLES = { BCKG_COLOR: backgroundColor => ({ backgroundColor: backgroundColor }) }; -//----------------------- -//Вспомогательные функции -//----------------------- - //--------------- //Тело компонента //--------------- -//Диалоговое окно фильтра отбора +//Диалог настройки карточки событий const TaskCardSettings = ({ initial, availableClrs, onCancel, onOk }) => { //Собственное состояние const [settings, setSettings] = useState({ ...initial }); @@ -112,7 +108,7 @@ const TaskCardSettings = ({ initial, availableClrs, onCancel, onOk }) => { ); }; -//Контроль свойств компонента - Диалоговое окно настройки карточки событий +//Контроль свойств компонента - Диалог настройки карточки событий TaskCardSettings.propTypes = { initial: PropTypes.object.isRequired, availableClrs: PropTypes.arrayOf(PropTypes.string).isRequired, diff --git a/app/panels/clnt_task_board/components/task_form.js b/app/panels/clnt_task_board/components/task_form.js index f04f289..b36caf8 100644 --- a/app/panels/clnt_task_board/components/task_form.js +++ b/app/panels/clnt_task_board/components/task_form.js @@ -7,16 +7,28 @@ //Подключение библиотек //--------------------- -import React, { useState } from "react"; //Классы React +import React, { useState, useContext, useEffect } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Box, Typography, TextField, Dialog, DialogContent, DialogActions, Button, Tabs, Tab, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты -import { useClientEvent } from "../hooks"; //Вспомогательные хуки +import { useClientEvent, useDocsProps } from "../hooks"; //Вспомогательные хуки import { APP_STYLES } from "../../../../app.styles"; //Типовые стили +import { ApplicationСtx } from "../../../context/application"; //Контекст приложения +import dayjs from "dayjs"; //Работа с датами +import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты //--------- //Константы //--------- +//Перечисление "Значение по умолчанию" +const DP_DEFAULT_VALUE = Object.freeze({ 0: "defaultStr", 1: "defaultNum", 2: "defaultDate", 3: "defaultNum" }); +//Перечисление "Префикс формата данных" +const DP_TYPE_PREFIX = Object.freeze({ 0: "S", 1: "N", 2: "D", 3: "N" }); +//Перечисление "Входящее значение дополнительного словаря" +const DP_IN_VALUE = Object.freeze({ 0: "pos_str_value", 1: "pos_num_value", 2: "pos_date_value", 3: "pos_num_value" }); +//Перечисление "Исходящее значение дополнительного словаря" +const DP_RETURN_VALUE = Object.freeze({ 0: "str_value", 1: "num_value", 2: "date_value", 3: "num_value" }); + //Стили const STYLES = { CONTAINER: { margin: "5px 0px", textAlign: "center" }, @@ -52,6 +64,9 @@ const STYLES = { //Вспомогательные функции и компоненты //------------------------------------ +//Подключение настройки пользовательского формата даты +dayjs.extend(customParseFormat); + //Свойства вкладки function a11yProps(index) { return { @@ -205,7 +220,7 @@ const MainEventInfoTab = ({ //Контроль свойств - Вкладка основной информации MainEventInfoTab.propTypes = { task: PropTypes.object.isRequired, - editable: PropTypes.bool, + editable: PropTypes.bool.isRequired, handleFieldEdit: PropTypes.func.isRequired, handleClientClientsOpen: PropTypes.func.isRequired, handleClientPersonOpen: PropTypes.func.isRequired, @@ -220,12 +235,12 @@ const ExecutorEventInfoTab = ({ task, handleFieldEdit, handleClientPersonOpen }) Планирование @@ -336,15 +351,192 @@ ExecutorEventInfoTab.propTypes = { handleClientPersonOpen: PropTypes.func.isRequired }; +//Вкладка информации со свойствами +const PropsEventInfoTab = ({ task, docProps, handlePropEdit }) => { + //Подключение к контексту приложения + const { pOnlineShowDictionary } = useContext(ApplicationСtx); + + //Формат дополнительного свойства типа число (длина, точность) + const DPNumFormat = (l, p) => new RegExp("^(\\d{1," + (l - p) + "}" + (p > 0 ? "((\\.|,)\\d{1," + p + "})?" : "") + ")?$"); + + //Формат дополнительного свойства типа строка (длина) + const DPStrFormat = l => new RegExp("^.{0," + l + "}$"); + + //Проверка валидности числа + const isValidDPNum = (length, prec, value) => { + return DPNumFormat(length, prec).test(value); + }; + + //Проверка валидности строки + const isValidDPStr = (length, value) => { + return DPStrFormat(length).test(value); + }; + + //Признак ошибки валидации + const validationError = (value = "", format, numW, numPrec, strW) => { + if (format === 0) return isValidDPStr(strW, value); + else if (format === 1) { + return isValidDPNum(numW, numPrec, value); + } else return true; + }; + + //Конвертация времени в привычный формат + const timeFromSqlFormat = ts => { + if (ts.indexOf(".") !== -1) { + let s = 24 * 60 * 60 * ts; + const h = Math.trunc(s / (60 * 60)); + s = s % (60 * 60); + const m = Math.trunc(s / 60); + s = Math.round(s % 60); + const formattedTime = ("0" + h).slice(-2) + ":" + ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2); + return formattedTime; + } + return ts; + }; + + //Выбор из словаря или дополнительного словаря + const handleDict = async (dp, curValue = null) => { + dp.entryType === 1 + ? pOnlineShowDictionary({ + unitCode: dp.unitcode, + showMethod: dp.showMethodCode, + inputParameters: dp.paramRn ? [{ name: dp.paramIn, value: curValue }] : null, + callBack: res => { + res.success + ? handlePropEdit({ + target: { + id: `${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`, + value: res.outParameters[dp.paramOut] + } + }) + : null; + } + }) + : pOnlineShowDictionary({ + unitCode: "ExtraDictionaries", + showMethod: "values", + inputParameters: [ + { name: "pos_rn", value: dp.extraDictRn }, + { name: DP_IN_VALUE[dp.format], value: curValue } + ], + callBack: res => { + res.success + ? handlePropEdit({ + target: { + id: `${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`, + value: res.outParameters[DP_RETURN_VALUE[dp.format]] + } + }) + : null; + } + }); + }; + + //Инициализация дополнительного свойства + const initProp = prop => { + //Значение свойства + const value = task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`]; + if ( + (task.nrn || task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`]) && + task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`] !== undefined + ) { + //Строка или число + if (prop.format < 2) return prop.numPrecision ? String(value).replace(".", ",") : value; + //Дата + else if (prop.format === 2) { + //Дата без времени + if (prop.dataSubtype === 0) return dayjs(value).format("YYYY-MM-DD"); + //Дата + время без секунд + else if (prop.dataSubtype === 1) return dayjs(value).format("YYYY-MM-DD HH:mm"); + //Дата + время с секундами + else return dayjs(value).format("YYYY-MM-DD HH:mm:ss"); + } + //Время + else { + return timeFromSqlFormat(value); + } + } else if (task.nrn) { + return ""; + } else return prop[DP_DEFAULT_VALUE[prop.format]]; + }; + + //Генерация содержимого + return ( + + + {docProps.props.map(dp => { + return dp.showInGrid ? ( + 0 + ? getInputProps(() => handleDict(dp, task.docProps[`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`])) + : null + } + InputLabelProps={ + dp.format < 2 + ? {} + : { + shrink: true + } + } + required={dp.require} + disabled={dp.readonly} + /> + ) : null; + })} + + + ); +}; + +//Контроль свойств - Вкладка информации со свойствами +PropsEventInfoTab.propTypes = { + task: PropTypes.object.isRequired, + docProps: PropTypes.object.isRequired, + handlePropEdit: PropTypes.func.isRequired +}; + //----------- //Тело модуля //----------- //Форма события -const TaskForm = ({ task, setTask, editable, handleClientClientsOpen, handleClientPersonOpen, handleCrnOpen, handleEventNextNumbGet }) => { +const TaskForm = ({ + task, + taskType, + setTask, + editable, + handleClientClientsOpen, + handleClientPersonOpen, + handleCrnOpen, + handleEventNextNumbGet, + handleDPReady +}) => { //Состояние вкладки const [value, setValue] = useState(0); + //Состояние допустимых дополнительных свойств + const [docProps] = useDocsProps(taskType); + //При изменении вкладки const handleChange = (event, newValue) => { setValue(newValue); @@ -361,6 +553,21 @@ const TaskForm = ({ task, setTask, editable, handleClientClientsOpen, handleClie })); }; + //При изменении свойства + const handlePropEdit = e => { + setTask(pv => ({ + ...pv, + docProps: { ...pv.docProps, [e.target.id]: e.target.value } + })); + }; + + //При заполнении всех обязательных свойств + useEffect(() => { + let i = 0; + docProps.props.filter(dp => dp.require === true).map(prop => (!task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`] ? i++ : null)); + docProps.loaded && i === 0 ? handleDPReady(true) : handleDPReady(false); + }, [docProps, handleDPReady, task.docProps]); + //Генерация содержимого return ( @@ -370,11 +577,12 @@ const TaskForm = ({ task, setTask, editable, handleClientClientsOpen, handleClie + {docProps.props.length > 0 ? : null} + {docProps.props.length > 0 ? ( + + + + ) : null} ); }; @@ -392,41 +605,57 @@ const TaskForm = ({ task, setTask, editable, handleClientClientsOpen, handleClie //Контроль свойств - Форма события TaskForm.propTypes = { task: PropTypes.object.isRequired, + taskType: PropTypes.string.isRequired, setTask: PropTypes.func.isRequired, - editable: PropTypes.bool, + editable: PropTypes.bool.isRequired, handleClientClientsOpen: PropTypes.func.isRequired, handleClientPersonOpen: PropTypes.func.isRequired, handleCrnOpen: PropTypes.func.isRequired, - handleEventNextNumbGet: PropTypes.func.isRequired + handleEventNextNumbGet: PropTypes.func.isRequired, + handleDPReady: PropTypes.func.isRequired }; //Диалог с формой события -const TaskFormDialog = ({ taskRn, taskType, taskStatus, editable, onClose }) => { +const TaskFormDialog = ({ taskRn, taskType, taskStatus, editable, handleReload, onClose }) => { //Собственное состояние const [task, setTask, insertEvent, updateEvent, handleClientClientsOpen, handleClientPersonOpen, handleCrnOpen, handleEventNextNumbGet] = useClientEvent(taskRn, taskType, taskStatus); + //Состояние заполненности всех обязательных свойств + const [dpReady, setDPReady] = useState(false); + + //Изменение состояния заполненности всех обязательных свойств + const handleDPReady = v => setDPReady(v); + + //Генерация содержимого return ( {onClose ? ( {taskRn ? ( - ) : ( - )} @@ -440,9 +669,10 @@ const TaskFormDialog = ({ taskRn, taskType, taskStatus, editable, onClose }) => //Контроль свойств - Диалог с формой события TaskFormDialog.propTypes = { taskRn: PropTypes.number, - taskType: PropTypes.string, + taskType: PropTypes.string.isRequired, taskStatus: PropTypes.string, editable: PropTypes.bool, + handleReload: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired }; diff --git a/app/panels/clnt_task_board/hooks.js b/app/panels/clnt_task_board/hooks.js index 113e69a..ded11e5 100644 --- a/app/panels/clnt_task_board/hooks.js +++ b/app/panels/clnt_task_board/hooks.js @@ -12,7 +12,6 @@ import { ApplicationСtx } from "../../context/application"; //Контекст import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений import { object2Base64XML, deepCopyObject } from "../../core/utils"; //Вспомогательные функции -import dayjs from "dayjs"; //Работа с датами import { EVENT_STATES } from "./components/filter_dialog"; //Перечисление состояний события //--------- @@ -72,21 +71,6 @@ const useTasks = () => { //Состояние изменения настройки статуса const [cardSettings, setCardSettings] = useState({ isOpen: false, settings: {} }); - //Состояние маршрута события - const [eventRoutes, setEventRoutes] = useState([]); - - //Состояние точек маршрута события - const [eventPoints, setEventPoints] = useState([]); - - //Состояние типов заголовков событий - const [noteTypes, setNoteTypes] = useState([]); - - //Состояние учётных документов - const [docLinks, setDocLinks] = useState([]); - - //Состояние аккаунтов - const [accounts, setAccounts] = useState([]); - //Состояние событий const [tasks, setTasks] = useState({ groupsLoaded: false, @@ -117,6 +101,7 @@ const useTasks = () => { { name: "NLINKED_RN", from: "", to: "" } ] }, + extraData: { typeLoaded: "", evRoutes: [], evPoints: [], noteTypes: [], docLinks: [], accounts: [] }, rows: [], statuses: [], openCardForm: false, @@ -131,6 +116,11 @@ const useTasks = () => { //Инициализация параметров события const initTask = (id, gp, task) => { + //Добавление дополнительных свойств + let newDocProps = {}; + Object.keys(task) + .filter(k => k.includes("DP_")) + .map(dp => (newDocProps = { ...newDocProps, [dp]: task[dp] })); return { id: id, name: task.SPREF_NUMB, @@ -146,6 +136,7 @@ const useTasks = () => { sclnt_clnperson: "", dchange_date: task.DCHANGE_DATE, dstart_date: task.DREG_DATE, + dexpire_date: task.DEXPIRE_DATE, dplan_date: task.DPLAN_DATE, sinit_clnperson: task.SINIT_PERSON, sinit_user: "", @@ -166,7 +157,11 @@ const useTasks = () => { sto_user: "", //SEND_USER_GROUP sto_usergrp: task.SSEND_USRGRP, - scurrent_user: "" + sSender: task.SSENDER, + scurrent_user: "", + slinked_unit: task.SLINKED_UNIT, + nlinked_rn: task.NLINKED_RN, + docProps: newDocProps }; }; @@ -238,7 +233,7 @@ const useTasks = () => { tasks.filters.needSave ? window.addEventListener("beforeunload", function () { Object.keys(tasks.filters.values).map(function (k) { - k !== "docLink" ? localStorage.setItem(k, tasks.filters.values[k]) : null; + k !== "docLink" ? localStorage.setItem(k, tasks.filters.values[k] ? tasks.filters.values[k] : "") : null; }); }) : null; @@ -435,7 +430,7 @@ const useTasks = () => { const data = await executeStored({ stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DOCLINKS", args: { - SCODE: type + SEVNTYPE_CODE: type }, respArg: "COUT" }); @@ -447,79 +442,16 @@ const useTasks = () => { newDocLinks.push({ id: d.NRN, descr: d.SDESCR }); }); } - //Указываем сформированные учётные документы - setDocLinks([...newDocLinks]); + //Возвращаем сформированные учётные документы return newDocLinks; }, [executeStored, tasks.filters.values.type] ); - useEffect(() => { - //Считывание вспомогательных данных - let getEventData = async () => { - const data = await executeStored({ - stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INFO_BY_CODE", - args: { - SCODE: tasks.filters.values.type - }, - respArg: "COUT" - }); - //Инициализируем маршруты событий - let newRoutes = []; - //Если найдены маршруты - if (data.XEVROUTES) { - arrayFormer(data.XEVROUTES).map(r => { - newRoutes.push({ src: r.SSOURCE, dest: r.SDESTINATION }); - }); - } - //Инициализируем точки событий - let newPoints = []; - if (data.XEVPOINTS) { - arrayFormer(data.XEVPOINTS).map(p => { - newPoints.push({ point: p.SEVPOINT, addNoteOnChst: p.ADDNOTE_ONCHST, addNoteOnSend: p.ADDNOTE_ONSEND, banUpdate: p.BAN_UPDATE }); - }); - } - //Инициализируем типы заголовков примечаний - let newNoteTypes = []; - if (data.XNOTETYPES) { - arrayFormer(data.XNOTETYPES).map(nt => { - newNoteTypes.push(nt.SNAME); - }); - } - //Инициализируем пользователей - let newAccounts = []; - //Если найдены пользователи - if (data.XACCOUNTS) { - arrayFormer(data.XACCOUNTS).map(a => { - newAccounts.push({ - agnAbbr: a.SAGNABBR, - image: a.BIMAGE, - evRnList: a.SEVRN_LIST.toString().includes(";") ? a.SEVRN_LIST.toString().split(";") : [a.SEVRN_LIST.toString()] - }); - }); - } - //Указываем сформированные маршруты - setEventRoutes([...newRoutes]); - //Указываем сформированные точки маршрута - setEventPoints([...newPoints]); - //Указываем типы заголовков примечаний - setNoteTypes([...newNoteTypes]); - //Указываем сформированные аккаунты - setAccounts([...newAccounts]); - }; - //Если указан тип событий - if (tasks.filters.values.type) { - //Загружаем данные - getEventData(); - //Загружаем учётные документы - getDocLinks(); - } - }, [tasks.filters.values.type, executeStored, getDocLinks]); - useEffect(() => { //Считывание данных с учетом фильтрации let getTasks = async () => { - const data = await executeStored({ + const ds = await executeStored({ stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DATASET", args: { CFILTERS: { VALUE: object2Base64XML(tasks.filters.fArray, { arrayNodeName: "filters" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB }, @@ -532,53 +464,124 @@ const useTasks = () => { let newGroups = []; let newRows = []; //Если статусы есть - if (data.XGROUPS) { + if (ds.XDATA_GRID.groups) { //Формируем структуру статусов - arrayFormer(data.XGROUPS).map((group, i) => { - newGroups.push({ id: i, code: group.name, caption: group.caption, color: randomColor(i + 1) }); + arrayFormer(ds.XDATA_GRID.groups).map((group, i) => { + newGroups.push({ id: i, code: group.name, name: group.caption, color: randomColor(i + 1) }); }); //Если есть события - if (data.XROWS) { + if (ds.XDATA_GRID.rows) { //Формируем структуру событий - arrayFormer(data.XROWS).map((task, i) => { - newRows.push(initTask(i, newGroups.find(x => x.caption === task.groupName).id, task)); + arrayFormer(ds.XDATA_GRID.rows).map((task, i) => { + newRows.push(initTask(i, newGroups.find(x => x.name === task.groupName).id, task)); }); } } - //Указываем сформированные данные + //Возвращаем информацию + return { statuses: [...newGroups], rows: [...newRows] }; + }; + //Считывание вспомогательных данных + let getEventData = async () => { + const data = await executeStored({ + stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INFO_BY_CODE", + args: { + SEVNTYPE_CODE: tasks.filters.values.type + }, + respArg: "COUT" + }); + //Инициализируем маршруты событий + let newRoutes = data.XEVROUTES + ? arrayFormer(data.XEVROUTES).reduce((prev, cur) => { + prev.push({ src: cur.SSOURCE, dest: cur.SDESTINATION }); + return prev; + }, []) + : []; + //Инициализируем точки событий + let newPoints = data.XEVPOINTS + ? arrayFormer(data.XEVPOINTS).reduce((prev, cur) => { + prev.push({ + point: cur.SEVPOINT, + pointDescr: cur.SEVPOINT_DESCR, + addNoteOnChst: cur.ADDNOTE_ONCHST, + addNoteOnSend: cur.ADDNOTE_ONSEND, + banUpdate: cur.BAN_UPDATE + }); + return prev; + }, []) + : []; + //Инициализируем типы заголовков примечаний + let newNoteTypes = data.XNOTETYPES + ? arrayFormer(data.XNOTETYPES).reduce((prev, cur) => { + prev.push(cur.SNAME); + return prev; + }, []) + : []; + //Инициализируем пользователей + let newAccounts = data.XACCOUNTS + ? arrayFormer(data.XACCOUNTS).reduce((prev, cur) => { + prev.push({ + agnAbbr: cur.SAGNABBR, + image: cur.BIMAGE + }); + return prev; + }, []) + : []; + //Загружаем учётные документы + let docLinks = await getDocLinks(tasks.filters.values.type); + //Возвращаем результат + return { + typeLoaded: tasks.filters.values.type, + evRoutes: [...newRoutes], + evPoints: [...newPoints], + noteTypes: [...newNoteTypes], + docLinks: [...docLinks], + accounts: [...newAccounts] + }; + }; + //Считывание данных + let getData = async () => { + //Инициализируем информацию о типе событии + let eventData = { ...tasks.extraData }; + //Если необходимо обновить информацию о типе события + if (!tasks.extraData.typeLoaded || tasks.filters.values.type !== tasks.extraData.typeLoaded) { + //Загружаем информацию о типе события + eventData = await getEventData(); + } + //Считываем информацию о задачах + let eventTasks = await getTasks(); + //Добавление описания точки маршрута + eventTasks.statuses.map(s => (s["pointDescr"] = eventData.evPoints.find(ep => ep.point === s.code).pointDescr)); + //Загружаем данные setTasks(pv => ({ ...pv, groupsLoaded: true, tasksLoaded: true, - statuses: [...newGroups], - rows: [...newRows], + statuses: eventTasks.statuses, + rows: eventTasks.rows, + extraData: eventData, reload: false })); }; + //Если необходимо загрузить данные и указан тип событий if (tasks.reload && tasks.filters.values.type) { //Загружаем данные - getTasks(); + getData(); } }, [ tasks.reload, tasks.filters.values.type, - tasks.filters.fArray, tasks.orders, - tasks.tasksLoaded, - tasks.statuses.length, - tasks.rows.length, executeStored, - SERV_DATA_TYPE_CLOB + SERV_DATA_TYPE_CLOB, + tasks.filters.fArray, + tasks.tasksLoaded, + tasks.extraData, + getDocLinks ]); return [ tasks, - eventRoutes, - eventPoints, - noteTypes, - docLinks, - accounts, taskFormOpen, setTaskFormOpen, cardSettings, @@ -595,6 +598,112 @@ const useTasks = () => { ]; }; +//Хук для получения пользовательских настроек разметки +const useColorRules = () => { + //Собственное состояние + const [clrRules, setClrRules] = useState({ loaded: false, rules: [] }); + + //Подключение к контексту взаимодействия с сервером + const { executeStored } = useContext(BackEndСtx); + + useEffect(() => { + let getClrRules = async () => { + //Получаем массив пользовательских настроек + const data = await executeStored({ + stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DP_RULES_GET", + respArg: "COUT" + }); + //Инициализируем + let newClrRules = []; + if (data) { + //Формируем структуру настройки + arrayFormer(data.XRULES).map((cr, i) => { + let fromV; + let toV; + if (cr.STYPE === "number") { + fromV = cr.NFROM; + toV = cr.NTO; + } else if (cr.STYPE === "string") { + fromV = cr.SFROM; + toV = cr.STO; + } else { + fromV = cr.DFROM; + toV = cr.DTO; + } + + newClrRules.push({ id: i, fieldCode: cr.SFIELD, propName: cr.SDP_NAME, color: cr.SCOLOR, vType: cr.STYPE, from: fromV, to: toV }); + }); + setClrRules({ loaded: true, rules: [...newClrRules] }); + } + }; + + if (!clrRules.loaded) getClrRules(); + }, [clrRules.loaded, executeStored]); + + return [clrRules]; +}; + +//Хук для получения свойств раздела "События" +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 }, + respArg: "COUT" + }); + //Инициализируем + let newDocProps = []; + if (data) { + //Формируем структуру настройки + arrayFormer(data.XPROPS).map((dp, i) => { + newDocProps.push({ + id: i, + rn: dp.NRN, + name: dp.SNAME, + readonly: dp.READONLY, + checkValue: dp.CHECK_VALUE, + checkUnique: dp.CHECK_UNIQUE, + require: dp.REQUIRE, + duplicateValue: dp.DUPLICATE_VALUE, + accessMode: dp.NACCESS_MODE, + showInGrid: dp.SHOW_IN_GRID, + defaultStr: dp.SDEFAULT_STR, + defaultNum: dp.NDEFAULT_NUM, + defaultDate: dp.DDEFAULT_DATE, + entryType: dp.NENTRY_TYPE, + format: dp.NFORMAT, + dataSubtype: dp.NDATA_SUBTYPE, + numWidth: dp.NNUM_WIDTH, + numPrecision: dp.NNUM_PRECISION, + strWidth: dp.NSTR_WIDTH, + unitcode: dp.SUNITCODE, + paramRn: dp.NPARAM_RN, + paramIn: dp.SPARAM_IN_CODE, + paramOut: dp.SPARAM_OUT_CODE, + showMethodRn: dp.NSHOW_METHOD_RN, + showMethodCode: dp.SMETHOD_CODE, + extraDictRn: dp.NEXTRA_DICT_RN, + initRn: dp.NINIT_RN + }); + }); + setDocsProps({ loaded: true, props: [...newDocProps] }); + } + }; + + if (!docProps.loaded) getDocsProps(); + }, [docProps.loaded, executeStored, taskType]); + + return [docProps]; +}; + //Хук для события const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { //Собственное состояние @@ -624,11 +733,12 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { scurrent_user: "", isUpdate: false, insertDisabled: true, - updateDisabled: true + updateDisabled: true, + docProps: {} }); //Подключение к контексту взаимодействия с сервером - const { executeStored } = useContext(BackEndСtx); + const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx); //Подключение к контексту приложения const { pOnlineShowDictionary } = useContext(ApplicationСtx); @@ -746,29 +856,46 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { SNUMB: task.snumber, STYPE: task.stype, SSTATUS: task.sstatus, - SPLAN_DATE: task.dstart_date ? dayjs(task.dstart_date).format("DD.MM.YYYY HH:mm") : null, + SPLAN_DATE: task.dplan_date, // ? dayjs(task.dplan_date).format("DD.MM.YYYY HH:mm") : null, SINIT_PERSON: task.sinit_clnperson, SCLIENT_CLIENT: task.sclnt_clnclients, SCLIENT_PERSON: task.sclnt_clnperson, SDESCRIPTION: task.sdescription, - SREASON: task.sinit_reason + SREASON: task.sinit_reason, + 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(); }, [ executeStored, - task.dstart_date, + task.scrn, + task.sprefix, + task.snumber, + task.stype, + task.sstatus, + task.dplan_date, + task.sinit_clnperson, task.sclnt_clnclients, task.sclnt_clnperson, - task.scrn, task.sdescription, - task.sinit_clnperson, task.sinit_reason, - task.snumber, - task.sprefix, - task.sstatus, - task.stype + task.docProps, + SERV_DATA_TYPE_CLOB ] ); @@ -781,12 +908,19 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { NCLNEVENTS: task.nrn, SCLIENT_CLIENT: task.sclnt_clnclients, SCLIENT_PERSON: task.sclnt_clnperson, - SDESCRIPTION: task.sdescription + 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(); }, - [executeStored, task.nrn, task.sclnt_clnclients, task.sclnt_clnperson, task.sdescription] + [SERV_DATA_TYPE_CLOB, executeStored, task.docProps, task.nrn, task.sclnt_clnclients, task.sclnt_clnperson, task.sdescription] ); useEffect(() => { @@ -801,6 +935,10 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { }, respArg: "COUT" }); + let newDocProps = {}; + Object.keys(data.XEVENT) + .filter(k => k.includes("DP_")) + .map(dp => (newDocProps = { ...newDocProps, [dp]: data.XEVENT[dp] })); setTask(pv => ({ ...pv, scrn: data.XEVENT.SCRN, @@ -811,7 +949,7 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { sdescription: data.XEVENT.SDESCRIPTION, sclnt_clnclients: data.XEVENT.SCLIENT_CLIENT, sclnt_clnperson: data.XEVENT.SCLIENT_PERSON, - dstart_date: data.XEVENT.SPLAN_DATE ? dayjs(data.XEVENT.SPLAN_DATE).format("YYYY-MM-DD HH:mm") : "", + dplan_date: data.XEVENT.SPLAN_DATE, sinit_clnperson: data.XEVENT.SINIT_PERSON, sinit_user: data.XEVENT.SINIT_AUTHID, sinit_reason: data.XEVENT.SREASON, @@ -825,7 +963,8 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { sto_usergrp: data.XEVENT.SSEND_USER_GROUP, scurrent_user: data.XEVENT.SINIT_AUTHID, isUpdate: true, - init: false + init: false, + docProps: newDocProps })); }; //Инициализация параметров события @@ -878,7 +1017,62 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => { return [task, setTask, insertEvent, updateEvent, handleClientClientsOpen, handleClientPersonOpen, handleCrnOpen, getEventNextNumb]; }; -//Карточка события +//Хук дополнительныч настроек +const useSettings = statuses => { + //Собственное состояние + const [settings, setSettings] = useState({ + open: false, + statusesSort: { + sorted: false, + attr: localStorage.getItem("settingsSortAttr") ? localStorage.getItem("settingsSortAttr") : "name", + dest: localStorage.getItem("settingsSortDest") ? localStorage.getItem("settingsSortDest") : "asc", + statuses: [] + }, + colorRule: localStorage.getItem("settingsColorRule") ? JSON.parse(localStorage.getItem("settingsColorRule")) : {} + }); + + //При открытии диалога дополнительных настроек + const settingsOpen = () => setSettings(pv => ({ ...pv, open: true })); + + //При закрытии диалога дополнительных настроек + const settingsClose = () => setSettings(pv => ({ ...pv, open: false })); + + //Изменение состояния после сортировки + const afterSort = statuses => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, sorted: true, statuses: statuses } })); + + //При закрытии диалога дополнительных настроек по кнопке ОК + const handleSettingsOk = s => { + setSettings({ ...s, open: false, statusesSort: { ...s.statusesSort, sorted: false } }); + }; + + //При получении новых настроек сортировки + useEffect(() => { + //Подгрузкка новых статусов + if (statuses.length > 0 && statuses.toString() !== settings.statusesSort.statuses.toString() && settings.statusesSort.sorted) + setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, sorted: false } })); + //Сортировка + if (statuses.length > 0 && !settings.statusesSort.sorted) { + const attr = settings.statusesSort.attr; + const d = settings.statusesSort.dest; + let s = statuses; + s.sort((a, b) => (d === "asc" ? a[attr].localeCompare(b[attr]) : b[attr].localeCompare(a[attr]))); + afterSort(s); + } + }, [settings.statusesSort.attr, settings.statusesSort.dest, settings.statusesSort.sorted, settings.statusesSort.statuses, statuses]); + + //Сохранение при закрытии панели + useEffect(() => { + window.addEventListener("beforeunload", function () { + localStorage.setItem("settingsSortAttr", settings.statusesSort.attr); + localStorage.setItem("settingsSortDest", settings.statusesSort.dest); + localStorage.setItem("settingsColorRule", JSON.stringify(settings.colorRule)); + }); + }, [settings.colorRule, settings.statusesSort.attr, settings.statusesSort.dest]); + + return [settings, settingsOpen, settingsClose, handleSettingsOk]; +}; + +//Хук карточки события const useTaskCard = () => { //Собственное состояние const [taskCard, setTaskCard] = useState({ openEdit: false }); @@ -1257,4 +1451,4 @@ const useOrders = () => { return [menuOrders, handleOrdersMenuButtonClick, handleOrdersMenuClose]; }; -export { useTasks, useClientEvent, useTaskCard, useOrders }; +export { useTasks, useSettings, useColorRules, useDocsProps, useClientEvent, useTaskCard, useOrders };