Панель "Доски задач" - добавление перенесено в заголовок, исправлены ошибки работы заливки событий по условиям

This commit is contained in:
Mikhail Chechnev 2025-10-23 17:39:50 +03:00
parent a7a883ecc6
commit 2ad4cfdba9
7 changed files with 170 additions and 178 deletions

View File

@ -76,9 +76,6 @@ const ClntTaskBoard = () => {
//Состояние доступных маршрутов события
const [availableRoutes, setAvailableRoutes] = useState({ source: "", routes: [] });
//Состояние перетаскиваемого события
const [dragItem, setDragItem] = useState({ type: "", status: "" });
//При открытии/закрытии диалога фильтра
const handleFilterOpen = isOpen => {
setDialogsState(pv => ({ ...pv, filterDialogIsOpen: isOpen }));
@ -152,18 +149,6 @@ const ClntTaskBoard = () => {
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;
@ -209,18 +194,17 @@ const ClntTaskBoard = () => {
/>
) : null}
{dialogsState.taskDialogIsOpen ? (
<TaskDialog
taskType={dragItem.type}
taskStatus={dragItem.status}
onTasksReload={() => handleTasksReload(true)}
onClose={() => {
handleTaskDialogOpen();
handleDragItemClear();
}}
/>
<TaskDialog taskType={filters.values.sType} onTasksReload={() => handleTasksReload(true)} onClose={handleTaskDialogOpen} />
) : null}
<Box sx={STYLES.BOX_FILTER}>
<Stack direction="row">
<Box>
<Stack direction="row" pl={1} pt={1}>
<IconButton onClick={handleTaskDialogOpen} title={"Добавить событие"}>
<Icon>add</Icon>
</IconButton>
</Stack>
</Box>
<Filter
isFilterDialogOpen={dialogsState.filterDialogIsOpen}
filter={filters.values}
@ -282,11 +266,8 @@ const ClntTaskBoard = () => {
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}

View File

@ -9,7 +9,7 @@
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 { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Icon, Button, Box, Stack, Typography } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { sortAttrs, sortDest } from "../layouts.js"; //Допустимые значение поля и направления сортировки
import { hasValue } from "../../../core/utils.js"; //Проверка наличия значения
@ -63,9 +63,23 @@ const SettingsDialog = ({ initial, onSettingsChange, onClose, ...other }) => {
<CustomInputField
elementCode="clrRules"
elementValue={hasValue(colorRules.selectedColorRule.id) && colorRules.length !== 0 ? colorRules.selectedColorRule.id : -1}
labelText="Заливка событий"
items={colorRules.rules.reduce((prev, cur) => [...prev, { id: cur.id, caption: cur.SDP_NAME }], [])}
emptyItem={{ key: -1, id: -1, caption: "-" }}
labelText="Заливка событий*"
items={colorRules.rules.reduce(
(prev, cur) => [
...prev,
{
id: cur.id,
caption:
`${cur.SDP_NAME}` +
(cur.STYPE == "string"
? `${cur.fromValue ? `, значение "${cur.fromValue}"` : ""}`
: `${cur.fromValue ? `, с ${cur.fromValue}` : ""}` + `${cur.toValue ? `, по ${cur.toValue}` : ""}`) +
`${cur.SCOLOR ? `, ${cur.SCOLOR}` : ""}`
}
],
[]
)}
emptyItem={{ key: -1, id: -1, caption: "Нет" }}
onChange={handleColorRuleChange}
sx={STYLES.SELECT}
/>
@ -75,7 +89,7 @@ const SettingsDialog = ({ initial, onSettingsChange, onClose, ...other }) => {
<CustomInputField
elementCode="attr"
elementValue={statusesState.attr}
labelText="Поле сортировки"
labelText="Порядок сортировки колонок"
items={sortAttrs.reduce((prev, cur) => [...prev, { id: cur.id, caption: cur.descr }], [])}
onChange={handleSortAttrChange}
sx={STYLES.SELECT}
@ -88,6 +102,10 @@ const SettingsDialog = ({ initial, onSettingsChange, onClose, ...other }) => {
</IconButton>
</Stack>
</Box>
<Typography variant={"caption"}>
*Поддерживаются правила заливки, базирующиеся на дополнительных свойствах типа &quot;Строка&quot; или &quot;Число&quot;, из
профиля пользователя, настроенного для раздела &quot;События&quot; в WEB-интерфейсе данного приложения.
</Typography>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button

View File

@ -9,7 +9,7 @@
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Card, CardHeader, CardContent, Button, IconButton, Icon, Typography, Stack } from "@mui/material"; //Интерфейсные компоненты
import { Card, CardHeader, CardContent, IconButton, Icon, Typography, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskCard } from "./task_card.js"; //Компонент Карточка события
import { StatusCardSettings } from "./status_card_settings.js"; //Компонент Диалог настройки карточки событий
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
@ -58,7 +58,7 @@ const STYLES = {
paddingRight: "5px",
paddingBottom: "5px !important",
overflowY: "auto",
maxHeight: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 85px)`,
maxHeight: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 55px)`,
...APP_STYLES.SCROLL
}
};
@ -74,11 +74,8 @@ const StatusCard = ({
statusTitle,
colorRules,
extraData,
filtersType,
isCardAvailable,
onTasksReload,
onDragItemChange,
onTaskDialogOpen,
onNoteDialogOpen,
onStatusColorChange,
placeholder
@ -123,16 +120,6 @@ const StatusCard = ({
{statusTitle}
</Typography>
}
subheader={
<Button
onClick={() => {
onDragItemChange(filtersType, status.SEVNSTAT_CODE);
onTaskDialogOpen();
}}
>
+ Добавить
</Button>
}
sx={STYLES.CARD_HEADER}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
@ -165,11 +152,8 @@ StatusCard.propTypes = {
statusTitle: PropTypes.string.isRequired,
colorRules: PropTypes.object.isRequired,
extraData: PropTypes.object.isRequired,
filtersType: PropTypes.string.isRequired,
isCardAvailable: PropTypes.func.isRequired,
onTasksReload: PropTypes.func.isRequired,
onDragItemChange: PropTypes.func.isRequired,
onTaskDialogOpen: PropTypes.func.isRequired,
onNoteDialogOpen: PropTypes.func.isRequired,
onStatusColorChange: PropTypes.func.isRequired,
placeholder: PropTypes.object.isRequired

View File

@ -15,7 +15,7 @@ import { BackEndСtx } from "../../../context/backend"; //Контекст вз
//-----------
//Хук для события
const useClientEvent = (taskRn, taskType = "", taskStatus = "") => {
const useClientEvent = (taskRn, taskType = "") => {
//Собственное состояние
const [task, setTask] = useState({
init: true,
@ -24,7 +24,7 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => {
sPrefix: "",
sNumber: "",
sType: taskType,
sStatus: taskStatus,
sStatus: "",
sDescription: "",
sClntClients: "",
sClntClnperson: "",
@ -118,6 +118,7 @@ const useClientEvent = (taskRn, taskType = "", taskStatus = "") => {
...pv,
sPrefix: data.SPREF,
sNumber: data.SNUMB,
sStatus: data.SSTATUS,
sCurrentUser: data.SINIT_AUTHNAME,
sInitClnperson: data.SINIT_PERSON,
sInitUser: !data.SINIT_PERSON ? data.SINIT_AUTHNAME : "",

View File

@ -12,8 +12,8 @@ export const EVENT_STATES = Object.freeze({ 0: "Все", 1: "Не аннулир
//Допустимые значение поля сортировки
export const sortAttrs = [
{ id: "SEVNSTAT_CODE", descr: "Мнемокод" },
{ id: "SEVNSTAT_NAME", descr: "Наименование" },
{ id: "SEVNSTAT_CODE", descr: "Мнемокод статуса" },
{ id: "SEVNSTAT_NAME", descr: "Наименование статуса" },
{ id: "SEVPOINT_DESCR", descr: "Описание точки маршрута" }
];
@ -73,24 +73,16 @@ const convertHexToRGB = hex => {
//Считывание заливки события по условию
export const getTaskBgColorByRule = (task, colorRule) => {
//Инициализируем значения
let ruleCode = "";
//Исходя из типа определяем наименование
//Исходя из типа определяем наименование и возвращаем цвет заливки
switch (colorRule.STYPE) {
case "number":
ruleCode = `N${colorRule.SFIELD}`;
break;
case "date":
ruleCode = `D${colorRule.SFIELD}`;
break;
return (!colorRule.fromValue || Number(task.docProps[`N${colorRule.SFIELD}`]) >= Number(colorRule.fromValue)) &&
(!colorRule.toValue || Number(task.docProps[`N${colorRule.SFIELD}`]) <= Number(colorRule.toValue))
? convertHexToRGB(colorRule.SCOLOR)
: null;
default:
ruleCode = `S${colorRule.SFIELD}`;
break;
return task.docProps[`S${colorRule.SFIELD}`] == colorRule.fromValue ? convertHexToRGB(colorRule.SCOLOR) : null;
}
//Определяем цвет заливки
let bgColor = ruleCode && task.docProps[ruleCode] == colorRule.fromValue ? convertHexToRGB(colorRule.SCOLOR) : null;
//Возвращаем цвет заливки
return bgColor;
};
//Индикация истечения срока отработки события

View File

@ -35,9 +35,9 @@ const STYLES = {
//-----------
//Диалог формы события
const TaskDialog = ({ taskRn, taskType, taskStatus, editable, onTasksReload, onClose }) => {
const TaskDialog = ({ taskRn, taskType, editable, onTasksReload, onClose }) => {
//Собственное состояние
const [task, setTask] = useClientEvent(taskRn, taskType, taskStatus);
const [task, setTask] = useClientEvent(taskRn, taskType);
//Состояние заполненности всех обязательных свойств
const [dpReady, setDPReady] = useState(false);
@ -167,7 +167,6 @@ const TaskDialog = ({ taskRn, taskType, taskStatus, editable, onTasksReload, onC
TaskDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
taskStatus: PropTypes.string,
editable: PropTypes.bool,
onTasksReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired

View File

@ -36,6 +36,7 @@ create or replace package PKG_P8PANELS_CLNTTSKBRD as
SEVENT_TYPE in varchar2, -- Мнемокод типа события
SPREF out varchar2, -- Префикс события
SNUMB out varchar2, -- Номер события
SSTATUS out varchar2, -- Начальный статус
SINIT_PERSON out varchar2, -- Сотрудник - инициатор
SINIT_AUTHNAME out varchar2 -- Пользователь - инициатор
);
@ -1000,8 +1001,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
RDG_ROW => RDG_ROW,
TDP_NAMES => TDP_NAMES,
TDP_TYPES => TDP_TYPES,
NFIRST_NUMB => RDG.RCOL_DEFS.COUNT - PKG_CONTVALLOC1S.COUNT_(RCONTAINER => TDP_NAMES) - 1,
NLAST_NUMB => RDG.RCOL_DEFS.COUNT - 2,
NFIRST_NUMB => RDG.RCOL_DEFS.COUNT - PKG_CONTVALLOC1S.COUNT_(RCONTAINER => TDP_NAMES),
NLAST_NUMB => RDG.RCOL_DEFS.COUNT - 1,
ICURSOR => ICURSOR,
NACTION => 2);
end if;
@ -1119,8 +1120,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
RDG_ROW => RDG_ROW,
TDP_NAMES => TDP_NAMES,
TDP_TYPES => TDP_TYPES,
NFIRST_NUMB => RDG.RCOL_DEFS.COUNT - PKG_CONTVALLOC1S.COUNT_(RCONTAINER => TDP_NAMES) - 1,
NLAST_NUMB => RDG.RCOL_DEFS.COUNT - 2,
NFIRST_NUMB => RDG.RCOL_DEFS.COUNT - PKG_CONTVALLOC1S.COUNT_(RCONTAINER => TDP_NAMES),
NLAST_NUMB => RDG.RCOL_DEFS.COUNT - 1,
ICURSOR => ICURSOR,
NACTION => 1);
end if;
@ -1457,11 +1458,13 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
SEVENT_TYPE in varchar2, -- Мнемокод типа события
SPREF out varchar2, -- Префикс события
SNUMB out varchar2, -- Номер события
SSTATUS out varchar2, -- Начальный статус
SINIT_PERSON out varchar2, -- Сотрудник - инициатор
SINIT_AUTHNAME out varchar2 -- Пользователь - инициатор
)
is
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса
NEVENT_TYPE PKG_STD.TREF; -- Рег. номер типа события
SOWNER_AGENT_RN PKG_STD.TSTRING; -- Рег. номер работодателя сотрудника
STAB_PREF PKG_STD.TSTRING; -- Префикс табельного номера сотрудника
STAB_NUMB PKG_STD.TSTRING; -- Таблеьный номер сотрудника
@ -1490,6 +1493,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
return SRESULT;
end CLNEVNTYPES_PREF_GET;
begin
/* Определяем рег. номер типа события */
FIND_CLNEVNTYPES_CODE(NFLAG_SMART => 0, NCOMPANY => NCOMPANY, SEVNTYPE_CODE => SEVENT_TYPE, NRN => NEVENT_TYPE);
/* Считываем префикс из параметра */
SPREF := GET_OPTIONS_STR(SCODE => 'EventPrefix', NCOMP_VERS => NCOMPANY);
/* Если префикс не указан в параметре и задан тип события */
@ -1502,6 +1507,21 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
/* Считываем номер */
SNUMB := GET_CLNEVENTS_NEXTNUMB(NCOMPANY => NCOMPANY, SPREF => SPREF);
end if;
/* Определим начальный статус */
begin
select T.SEVNSTAT_CODE
into SSTATUS
from V_CLNEVNTYPSTS_FOR_EVENTS T
where T.NPRN = NEVENT_TYPE
and ROWNUM <= 1
order by T.NDEFAULT_STATUS desc,
T.SEVNSTAT_CODE;
exception
when NO_DATA_FOUND then
P_EXCEPTION(0,
'Для событий типа "%s" не определена начальная точка маршрута.',
SEVENT_TYPE);
end;
/* Считываем сотрудника текущего пользователя */
FIND_PERSON_AUTHID(SPERSON => SINIT_PERSON,
SOWNER_AGENT => SOWNER_AGENT_RN,
@ -2259,6 +2279,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
is
SAUTHID PKG_STD.TSTRING := PKG_SESSION.GET_UTILIZER(); -- Пользователь
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Рег. номер организации
SAPPCODE PKG_STD.TSTRING := PKG_SESSION.GET_APPLICATION(); -- Код приложения сеанса
BXML blob; -- XML профиля пользователя для раздела "События"
RDOC PKG_XPATH.TDOCUMENT; -- Документ XML
RNODE_ROOT PKG_XPATH.TNODE; -- Корневой узел
@ -2275,8 +2296,6 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
NTO_VALUE PKG_STD.TNUMBER; -- Значение по (number)
SFROM_VALUE PKG_STD.TSTRING; -- Значение с (string)
STO_VALUE PKG_STD.TSTRING; -- Значение по (string)
DFROM_VALUE PKG_STD.TLDATE; -- Значение с (date)
DTO_VALUE PKG_STD.TLDATE; -- Значение по (date)
/* Считывание наименования свойства документа */
function DOCS_PROPS_NAME_GET
@ -2287,6 +2306,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
NRN PKG_STD.TREF; -- Рег. номер свойства документа
SRESULT PKG_STD.TSTRING; -- Наименование доп. свойства
begin
/* Если переданный код свойства соответствует формату */
if (INSTR(SFIELD, 'DP_') <> 0) then
/* Определяем рег. номер свойства документа по формату "DP_<РЕГ_НОМЕР>" */
NRN := TO_NUMBER(SUBSTR(SFIELD, INSTR(SFIELD, '_') + 1));
/* Считываем наименование из записи свойства документа */
@ -2300,6 +2321,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
P_EXCEPTION(0,
'Ошибка считывания наименования свойства документа с рег. номером (%s).');
end;
end if;
/* Возвращаем результат */
return SRESULT;
end DOCS_PROPS_NAME_GET;
@ -2308,6 +2330,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
function USERPROFILES_GET
(
NCOMPANY in number, -- Рег. номер организации
SAPPCODE in varchar2, -- Код приложения
SAUTHID in varchar2 -- Мнемокод пользователя
) return blob -- Профиль пользователя для раздела "События"
is
@ -2322,7 +2345,12 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
and U.AUTHID = SAUTHID
and U.REC_TYPE = 1
and U.UNITMODE = 0
and U.UNITCODE = SUNIT_CLNEVENTS;
and U.UNITCODE = SUNIT_CLNEVENTS
and U.KIND = 2
and U.UNITFUNC is null
and U.REC_KEY is null
and U.SHOW_METHOD = 'main'
and U.APPCODE = SAPPCODE;
exception
when others then
return null;
@ -2332,7 +2360,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
end USERPROFILES_GET;
begin
/* Считываем профиль пользователя для раздела "События" */
BXML := USERPROFILES_GET(NCOMPANY => NCOMPANY, SAUTHID => SAUTHID);
BXML := USERPROFILES_GET(NCOMPANY => NCOMPANY, SAPPCODE => SAPPCODE, SAUTHID => SAUTHID);
/* Проверим, что профиль не пустой */
if (BXML is null) then
return;
@ -2359,13 +2387,15 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
SFIELD := PKG_XPATH.ATTRIBUTE(RNODE => RNODE_RULE, SNAME => 'field');
SCOLOR := PKG_XPATH.ATTRIBUTE(RNODE => RNODE_RULE, SNAME => 'color');
SDATA_TYPE := PKG_XPATH.ATTRIBUTE(RNODE => RNODE_RULE, SNAME => 'dataType');
/* Считываем наименование свойства документа */
SDP_NAME := DOCS_PROPS_NAME_GET(SFIELD => SFIELD);
/* Если это правило по доп. свойству и типа "строка" или "число" */
if (SDP_NAME is not null) and (SDATA_TYPE in ('number', 'string')) then
PKG_XFAST.DOWN_NODE(SNAME => 'XRULES');
/* Код правила */
PKG_XFAST.DOWN_NODE(SNAME => 'SFIELD');
PKG_XFAST.VALUE(SVALUE => SFIELD);
PKG_XFAST.UP();
/* Считываем наименование свойства документа */
SDP_NAME := DOCS_PROPS_NAME_GET(SFIELD => SFIELD);
/* Наименование доп. свойства */
PKG_XFAST.DOWN_NODE(SNAME => 'SDP_NAME');
PKG_XFAST.VALUE(SVALUE => SDP_NAME);
@ -2397,9 +2427,9 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
/* Строка */
when 'string' then
SFROM_VALUE := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RNODE_RULE,
SPATTERN => 'fromValue'));
SPATTERN => 'value'));
STO_VALUE := PKG_XPATH.VALUE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RNODE_RULE,
SPATTERN => 'toValue'));
SPATTERN => 'value'));
/* Строка с */
PKG_XFAST.DOWN_NODE(SNAME => 'SFROM');
PKG_XFAST.VALUE(SVALUE => SFROM_VALUE);
@ -2408,22 +2438,9 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
PKG_XFAST.DOWN_NODE(SNAME => 'STO');
PKG_XFAST.VALUE(SVALUE => STO_VALUE);
PKG_XFAST.UP();
/* Дата */
else
DFROM_VALUE := PKG_XPATH.VALUE_DATE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RNODE_RULE,
SPATTERN => 'fromValue'));
DTO_VALUE := PKG_XPATH.VALUE_DATE(RNODE => PKG_XPATH.SINGLE_NODE(RPARENT_NODE => RNODE_RULE,
SPATTERN => 'toValue'));
/* Дата с */
PKG_XFAST.DOWN_NODE(SNAME => 'DFROM');
PKG_XFAST.VALUE(DVALUE => DFROM_VALUE);
PKG_XFAST.UP();
/* Дата по */
PKG_XFAST.DOWN_NODE(SNAME => 'DTO');
PKG_XFAST.VALUE(DVALUE => DTO_VALUE);
PKG_XFAST.UP();
end case;
PKG_XFAST.UP();
end if;
end loop;
PKG_XPATH.FREE(RDOCUMENT => RDOC);
PKG_XFAST.UP();