684 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Диалог формы события
*/
//---------------------
//Подключение библиотек
//---------------------
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, 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" },
DIALOG_CONTENT: {
paddingBottom: "0px",
maxHeight: "740px",
minHeight: "740px",
...APP_STYLES.SCROLL
},
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
BOX_WITH_LEGEND: { border: "1px solid #939393" },
BOX_SINGLE_COLUMN: { display: "flex", flexDirection: "column", gap: "10px" },
BOX_FEW_COLUMNS: { display: "flex", flexWrap: "wrap", justifyContent: "space-between" },
BOX_LEFT_ALIGN: { display: "flex", justifyContent: "flex-start" },
LEGEND: { textAlign: "left" },
TEXT_FIELD: (widthVal, greyDisabled = false) => ({
margin: "4px",
...(widthVal ? { width: widthVal } : {}),
...(greyDisabled
? {
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.87)"
},
"& .MuiInputLabel-root.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.6)"
}
}
: {})
})
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//Свойства вкладки
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
//Формирование кнопки для открытия раздела
const getInputProps = (onClick, disabled = false, icon = "list") => {
return {
endAdornment: (
<InputAdornment position="end">
<IconButton disabled={disabled} aria-label={`select`} onClick={onClick} edge="end">
<Icon>{icon}</Icon>
</IconButton>
</InputAdornment>
)
};
};
//Вкладка информации
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
return (
<Box role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other}>
{value === index && <Box pt={1}>{children}</Box>}
</Box>
);
}
//Контроль свойств - Вкладка информации
CustomTabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
//Вкладка основной информации
const MainEventInfoTab = ({
task,
editable,
handleFieldEdit,
handleClientClientsOpen,
handleClientPersonOpen,
handleCrnOpen,
handleEventNextNumbGet
}) => {
return (
<Box>
<Box sx={STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={STYLES.LEGEND}>Событие</legend>
<Box sx={STYLES.BOX_FEW_COLUMNS}>
<TextField
sx={STYLES.TEXT_FIELD()}
id="scrn"
label="Каталог"
fullWidth
value={task.scrn}
variant="standard"
onChange={handleFieldEdit}
InputProps={getInputProps(handleCrnOpen)}
required
disabled={task.isUpdate}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px")}
id="sprefix"
label="Префикс"
value={task.sprefix}
variant="standard"
onChange={handleFieldEdit}
required
disabled={task.isUpdate}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px")}
id="snumber"
label="Номер"
value={task.snumber}
variant="standard"
onChange={handleFieldEdit}
required
disabled={task.isUpdate}
InputProps={getInputProps(handleEventNextNumbGet, !task.sprefix || task.isUpdate, "refresh")}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px", !task.isUpdate)}
id="stype"
label="Тип"
value={task.stype}
variant="standard"
onChange={handleFieldEdit}
disabled
required
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px", !task.isUpdate)}
id="sstatus"
label="Статус"
value={task.sstatus}
variant="standard"
disabled
required
onChange={handleFieldEdit}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD()}
fullWidth
id="sdescription"
label="Описание"
value={task.sdescription}
variant="standard"
onChange={handleFieldEdit}
disabled={!task.stype || !editable}
required
multiline
minRows={7}
maxRows={7}
></TextField>
</Box>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Клиент</legend>
<TextField
sx={STYLES.TEXT_FIELD()}
id="sclnt_clnclients"
label="Организация"
value={task.sclnt_clnclients}
variant="standard"
onChange={handleFieldEdit}
disabled={!task.stype}
InputProps={getInputProps(handleClientClientsOpen, !task.stype)}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD()}
id="sclnt_clnperson"
label="Сотрудник"
value={task.sclnt_clnperson}
variant="standard"
onChange={handleFieldEdit}
disabled={!task.stype}
InputProps={getInputProps(() => handleClientPersonOpen(0), !task.stype)}
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка основной информации
MainEventInfoTab.propTypes = {
task: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
handleFieldEdit: PropTypes.func.isRequired,
handleClientClientsOpen: PropTypes.func.isRequired,
handleClientPersonOpen: PropTypes.func.isRequired,
handleCrnOpen: PropTypes.func.isRequired,
handleEventNextNumbGet: PropTypes.func.isRequired
};
//Вкладка информации об исполнителе
const ExecutorEventInfoTab = ({ task, handleFieldEdit, handleClientPersonOpen }) => {
return (
<Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_LEFT_ALIGN }} component="fieldset">
<legend style={STYLES.LEGEND}>Планирование</legend>
<TextField
id="dplan_date"
label="Начало работ"
InputLabelProps={{ shrink: true }}
type="datetime-local"
variant="standard"
value={task.dplan_date ? dayjs(task.dplan_date, "DD.MM.YYYY HH:mm").format("YYYY-MM-DD HH:mm") : ""}
onChange={handleFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Инициатор</legend>
<TextField
id="sinit_clnperson"
label="Сотрудник"
value={task.sinit_clnperson}
variant="standard"
onChange={handleFieldEdit}
disabled={task.isUpdate}
InputProps={getInputProps(() => handleClientPersonOpen(1), task.isUpdate)}
></TextField>
<TextField
id="sinit_user"
label="Пользователь"
value={task.sinit_user}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sinit_reason"
label="Основание"
value={task.sinit_reason}
variant="standard"
onChange={handleFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Направить</legend>
<TextField
id="sto_company"
label="Организация"
value={task.sto_company}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_department"
label="Подразделение"
value={task.sto_department}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_clnpost"
label="Должность"
value={task.sto_clnpost}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_clnpsdep"
label="Штатная должность"
value={task.sto_clnpsdep}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_clnperson"
label="Сотрудник"
value={task.sto_clnperson}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_fcstaffgrp"
label="Нештатная должность"
value={task.sto_fcstaffgrp}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_user"
label="Пользователь"
value={task.sto_user}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
<TextField
id="sto_usergrp"
label="Группа пользователей"
value={task.sto_usergrp}
variant="standard"
onChange={handleFieldEdit}
disabled
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации об исполнителе
ExecutorEventInfoTab.propTypes = {
task: PropTypes.object.isRequired,
handleFieldEdit: PropTypes.func.isRequired,
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 (
<Box>
<Box sx={STYLES.BOX_WITH_LEGEND} component="fieldset">
{docProps.props.map(dp => {
return dp.showInGrid ? (
<TextField
error={
!validationError(
task.docProps[`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`],
dp.format,
dp.numWidth,
dp.numPrecision,
dp.strWidth
)
}
key={dp.id}
sx={STYLES.TEXT_FIELD()}
id={`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`}
type={dp.format < 2 ? "string" : dp.format === 2 ? (dp.dataSubtype === 0 ? "date" : "datetime-local") : "time"}
label={dp.name}
fullWidth
value={initProp(dp)}
variant="standard"
onChange={handlePropEdit}
inputProps={(dp.format === 2 && dp.dataSubtype === 2) || (dp.format === 3 && dp.dataSubtype === 1) ? { step: 1 } : {}}
InputProps={
dp.entryType > 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;
})}
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации со свойствами
PropsEventInfoTab.propTypes = {
task: PropTypes.object.isRequired,
docProps: PropTypes.object.isRequired,
handlePropEdit: PropTypes.func.isRequired
};
//-----------
//Тело модуля
//-----------
//Форма события
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);
};
//При изменении поля
const handleFieldEdit = e => {
setTask(pv => ({
...pv,
[e.target.id]: e.target.value,
//Связанные значения, если меняется одно, то необходимо обнулить другое
...(e.target.id === "sclnt_clnperson" ? { sclnt_clnclients: "" } : {}),
...(e.target.id === "sclnt_clnclients" ? { sclnt_clnperson: "" } : {})
}));
};
//При изменении свойства
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 (
<Box sx={STYLES.CONTAINER}>
<Typography pb={1} variant="h6">
{task.nrn ? "Исправление события" : "Добавление события"}
</Typography>
<Tabs value={value} onChange={handleChange} aria-label="tabs of values">
<Tab label="Событие" {...a11yProps(0)} />
<Tab label="Исполнитель" {...a11yProps(1)} />
{docProps.props.length > 0 ? <Tab label="Свойства" {...a11yProps(2)} /> : null}
</Tabs>
<CustomTabPanel value={value} index={0}>
<MainEventInfoTab
task={task}
editable={editable}
handleFieldEdit={handleFieldEdit}
handleClientClientsOpen={handleClientClientsOpen}
handleClientPersonOpen={handleClientPersonOpen}
handleCrnOpen={handleCrnOpen}
handleEventNextNumbGet={handleEventNextNumbGet}
/>
</CustomTabPanel>
<CustomTabPanel value={value} index={1}>
<ExecutorEventInfoTab task={task} handleFieldEdit={handleFieldEdit} handleClientPersonOpen={handleClientPersonOpen} />
</CustomTabPanel>
{docProps.props.length > 0 ? (
<CustomTabPanel value={value} index={2}>
<PropsEventInfoTab task={task} taskType={taskType} docProps={docProps} handlePropEdit={handlePropEdit} />
</CustomTabPanel>
) : null}
</Box>
);
};
//Контроль свойств - Форма события
TaskForm.propTypes = {
task: PropTypes.object.isRequired,
taskType: PropTypes.string.isRequired,
setTask: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
handleClientClientsOpen: PropTypes.func.isRequired,
handleClientPersonOpen: PropTypes.func.isRequired,
handleCrnOpen: PropTypes.func.isRequired,
handleEventNextNumbGet: PropTypes.func.isRequired,
handleDPReady: PropTypes.func.isRequired
};
//Диалог с формой события
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 (
<Dialog open onClose={onClose ? onClose : null} fullWidth>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<TaskForm
task={task}
taskType={taskType}
setTask={setTask}
editable={!taskRn || editable ? true : false}
handleClientClientsOpen={handleClientClientsOpen}
handleClientPersonOpen={handleClientPersonOpen}
handleCrnOpen={handleCrnOpen}
handleEventNextNumbGet={handleEventNextNumbGet}
handleDPReady={handleDPReady}
/>
</DialogContent>
{onClose ? (
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
{taskRn ? (
<Button onClick={() => updateEvent(onClose).then(handleReload)} disabled={task.updateDisabled || !editable || !dpReady}>
Исправить
</Button>
) : (
<Button
onClick={() => {
insertEvent(onClose).then(handleReload);
}}
disabled={task.insertDisabled || !dpReady}
>
Добавить
</Button>
)}
<Button onClick={onClose}>Закрыть</Button>
</DialogActions>
) : null}
</Dialog>
);
};
//Контроль свойств - Диалог с формой события
TaskFormDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
taskStatus: PropTypes.string,
editable: PropTypes.bool,
handleReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormDialog };