forked from CITKParus/P8-Panels
543 lines
23 KiB
JavaScript
543 lines
23 KiB
JavaScript
/*
|
||
Парус 8 - Панели мониторинга - ПУП - Работы проектов
|
||
Панель мониторинга: Корневая панель работ проектов
|
||
*/
|
||
|
||
//---------------------
|
||
//Подключение библиотек
|
||
//---------------------
|
||
|
||
import React, { useContext, useState, useCallback, useEffect } from "react"; //Классы React
|
||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||
import {
|
||
Drawer,
|
||
Fab,
|
||
Box,
|
||
Grid,
|
||
List,
|
||
ListItemButton,
|
||
ListItemText,
|
||
ListItemIcon,
|
||
Icon,
|
||
Typography,
|
||
Divider,
|
||
ListItem,
|
||
Button,
|
||
Dialog,
|
||
DialogContent,
|
||
DialogActions,
|
||
TextField,
|
||
DialogTitle
|
||
} from "@mui/material"; //Интерфейсные элементы
|
||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||
import { formatDateJSONDateOnly } from "../../core/utils"; //Вспомогательные функции
|
||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
|
||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||
import { P8P_GANTT_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||
import { P8PGantt } from "../../components/p8p_gantt"; //Диаграмма Ганта
|
||
import { formatDateRF } from "../../core/utils"; //Вспомогательные функции
|
||
import { BUTTONS } from "../../../app.text"; //Текстовые ресурсы и константы
|
||
import { ResMon } from "./res_mon"; //Монитор ресурсов
|
||
import { taskAttributeRenderer } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||
|
||
//---------
|
||
//Константы
|
||
//---------
|
||
|
||
//Стили
|
||
const STYLES = {
|
||
PROJECTS_LIST_ITEM_NOJOBS: { backgroundColor: "#ff000045" },
|
||
PROJECTS_LIST_ITEM_PRIMARY: { wordWrap: "break-word" },
|
||
PROJECTS_LIST_ITEM_SECONDARY: { wordWrap: "break-word", fontSize: "0.5rem", textTransform: "uppercase" },
|
||
PROJECTS_LIST_ITEM_SECONDARY_NOJOBS: { color: "red" },
|
||
PROJECTS_LIST_ITEM_SECONDARY_NOEDIT: { color: "gray" },
|
||
PROJECTS_LIST_ITEM_SECONDARY_CHANGED: { color: "green" },
|
||
PROJECTS_BUTTON: { position: "absolute", top: `calc(${APP_BAR_HEIGHT} + 16px)`, left: "16px" },
|
||
PROJECTS_DRAWER: { width: "250px", flexShrink: 0, [`& .MuiDrawer-paper`]: { width: "250px", boxSizing: "border-box", ...APP_STYLES.SCROLL } },
|
||
GANTT_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})`, width: "100vw", paddingTop: "24px" },
|
||
GANTT_TITLE: { paddingLeft: "150px", paddingRight: "150px" },
|
||
PERIODS_BUTTON: { position: "absolute", top: `calc(${APP_BAR_HEIGHT} + 16px)`, right: "16px" },
|
||
PERIODS_DRAWER: { width: "1200px", flexShrink: 0, [`& .MuiDrawer-paper`]: { width: "1200px", boxSizing: "border-box", ...APP_STYLES.SCROLL } }
|
||
};
|
||
|
||
//------------------------------------
|
||
//Вспомогательные функции и компоненты
|
||
//------------------------------------
|
||
|
||
//Диалог параметров инициализации панели
|
||
const InitPrmsDialog = ({ dateBegin, dateFact, onOk, onCancel }) => {
|
||
//Собственное состояние - значения с-по
|
||
const [values, setValues] = useState({ dateBegin: formatDateJSONDateOnly(dateBegin), dateFact: formatDateJSONDateOnly(dateFact) });
|
||
|
||
//Отработка воода значения в фильтр
|
||
const handleValueTextFieldChanged = e => setValues(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||
|
||
//Генерация содержимого
|
||
return (
|
||
<Dialog
|
||
open={true}
|
||
aria-labelledby="init-dialog-title"
|
||
aria-describedby="init-dialog-description"
|
||
onClose={() => (onCancel ? onCancel() : null)}
|
||
>
|
||
<DialogTitle>Параметры инициализации</DialogTitle>
|
||
<DialogContent>
|
||
<TextField
|
||
style={{ padding: "10px" }}
|
||
name="dateBegin"
|
||
type="date"
|
||
value={values.dateBegin}
|
||
onChange={handleValueTextFieldChanged}
|
||
label="Начало (будет использован первый день месяца)"
|
||
variant="standard"
|
||
fullWidth
|
||
/>
|
||
<TextField
|
||
style={{ padding: "10px" }}
|
||
name="dateFact"
|
||
type="date"
|
||
value={values.dateFact}
|
||
onChange={handleValueTextFieldChanged}
|
||
label="Факт на (будет использован последний день месяца)"
|
||
variant="standard"
|
||
fullWidth
|
||
/>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => (onOk ? onOk({ dateBegin: new Date(values.dateBegin), dateFact: new Date(values.dateFact) }) : null)}>
|
||
{BUTTONS.OK}
|
||
</Button>
|
||
<Button onClick={() => (onCancel ? onCancel() : null)}>{BUTTONS.CANCEL}</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Диалог параметров инициализации панели
|
||
InitPrmsDialog.propTypes = {
|
||
dateBegin: PropTypes.instanceOf(Date).isRequired,
|
||
dateFact: PropTypes.instanceOf(Date).isRequired,
|
||
onOk: PropTypes.func,
|
||
onCancel: PropTypes.func
|
||
};
|
||
|
||
//Область параметров инициализации панели
|
||
const InitPrmsArea = ({ dateBegin, dateFact, durationMeasCode, labMeasCode, onClick }) => {
|
||
return (
|
||
<List>
|
||
<ListItem>
|
||
<ListItemText
|
||
secondary={
|
||
<>
|
||
<b>Начало: </b>
|
||
{formatDateRF(dateBegin)}
|
||
<br />
|
||
<b>Факт на: </b>
|
||
{formatDateRF(dateFact)}
|
||
<br />
|
||
<b>Длительность: </b>
|
||
{durationMeasCode}
|
||
<br />
|
||
<b>Трудоёмкость: </b>
|
||
{labMeasCode}
|
||
</>
|
||
}
|
||
/>
|
||
</ListItem>
|
||
<ListItem>
|
||
<Button fullWidth variant="contained" startIcon={<Icon>refresh</Icon>} onClick={onClick ? onClick : null}>
|
||
Переформировать...
|
||
</Button>
|
||
</ListItem>
|
||
</List>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Область параметров инициализации панели
|
||
InitPrmsArea.propTypes = {
|
||
dateBegin: PropTypes.instanceOf(Date),
|
||
dateFact: PropTypes.instanceOf(Date),
|
||
durationMeasCode: PropTypes.string,
|
||
labMeasCode: PropTypes.string,
|
||
onClick: PropTypes.func
|
||
};
|
||
|
||
//Область сохранения изменений
|
||
const SaveChangesArea = ({ onClick }) => {
|
||
return (
|
||
<List>
|
||
<ListItem>
|
||
<Button fullWidth color="warning" variant="contained" startIcon={<Icon>save</Icon>} onClick={onClick}>
|
||
Сохранить
|
||
</Button>
|
||
</ListItem>
|
||
</List>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Область сохранения изменений
|
||
SaveChangesArea.propTypes = {
|
||
onClick: PropTypes.func
|
||
};
|
||
|
||
//Список проектов
|
||
const ProjectsList = ({ projects = [], selectedProject, onClick } = {}) => {
|
||
//Подключение к контексту сообщений
|
||
const { InlineMsgErr } = useContext(MessagingСtx);
|
||
|
||
//Генерация содержимого
|
||
return projects.length > 0 ? (
|
||
<List>
|
||
{projects.map(p => (
|
||
<ListItemButton
|
||
key={p.NRN}
|
||
sx={p.NJOBS == 0 ? STYLES.PROJECTS_LIST_ITEM_NOJOBS : null}
|
||
selected={p.NRN === selectedProject}
|
||
onClick={() => (onClick ? onClick(p) : null)}
|
||
>
|
||
<ListItemIcon>
|
||
<Icon title={p.NEDITABLE == 1 ? "Можно редактировать" : "Редактирование недоступно"}>
|
||
{p.NEDITABLE == 1 ? "edit" : "edit_off"}
|
||
</Icon>
|
||
</ListItemIcon>
|
||
<ListItemText
|
||
primary={<Typography sx={STYLES.PROJECTS_LIST_ITEM_PRIMARY}>{p.SNAME}</Typography>}
|
||
secondary={
|
||
<Typography
|
||
sx={{
|
||
...STYLES.PROJECTS_LIST_ITEM_SECONDARY,
|
||
...(p.NJOBS == 0
|
||
? STYLES.PROJECTS_LIST_ITEM_SECONDARY_NOJOBS
|
||
: p.NCHANGED == 1
|
||
? STYLES.PROJECTS_LIST_ITEM_SECONDARY_CHANGED
|
||
: STYLES.PROJECTS_LIST_ITEM_SECONDARY_NOEDIT)
|
||
}}
|
||
>
|
||
{p.NJOBS == 1
|
||
? p.NEDITABLE == 1
|
||
? p.NCHANGED == 1
|
||
? "Изменён"
|
||
: "Не изменён"
|
||
: "Редактирование недоступно"
|
||
: "Работы не определены"}
|
||
</Typography>
|
||
}
|
||
/>
|
||
</ListItemButton>
|
||
))}
|
||
</List>
|
||
) : (
|
||
<InlineMsgErr okBtn={false} text={"Нет доступных проектов"} />
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Список проектов
|
||
ProjectsList.propTypes = {
|
||
projects: PropTypes.array,
|
||
selectedProject: PropTypes.number,
|
||
onClick: PropTypes.func
|
||
};
|
||
|
||
//-----------
|
||
//Тело модуля
|
||
//-----------
|
||
|
||
//Корневая панель работ проектов
|
||
const PrjJobs = () => {
|
||
//Собственное состояние
|
||
let [state, setState] = useState({
|
||
needSave: false,
|
||
showProjectsList: false,
|
||
showPeriodsList: false,
|
||
init: false,
|
||
dateBegin: null,
|
||
dateFact: null,
|
||
durationMeas: null,
|
||
durationMeasCode: null,
|
||
labMeas: null,
|
||
labMeasCode: null,
|
||
resourceStatus: null,
|
||
ident: null,
|
||
projects: [],
|
||
projectsLoaded: false,
|
||
selectedProjectJobsLoaded: false,
|
||
selectedProject: null,
|
||
selectedProjectDocRn: null,
|
||
selectedProjectGanttDef: {},
|
||
selectedProjectTasks: [],
|
||
showInitDialog: false
|
||
});
|
||
|
||
//Подключение к контексту приложения
|
||
const { pOnlineShowDocument } = useContext(ApplicationСtx);
|
||
|
||
//Подключение к контексту сообщений
|
||
const { InlineMsgInfo } = useContext(MessagingСtx);
|
||
|
||
//Подключение к контексту взаимодействия с сервером
|
||
const { executeStored } = useContext(BackEndСtx);
|
||
|
||
//Загрузка списка проектов
|
||
const loadProjects = useCallback(
|
||
async (force = false) => {
|
||
if (!state.projectsLoaded || force) {
|
||
const data = await executeStored({
|
||
stored: "PKG_P8PANELS_PROJECTS.JB_PRJCTS_LIST",
|
||
args: { NIDENT: state.ident },
|
||
respArg: "COUT",
|
||
isArray: name => name === "XPROJECTS"
|
||
});
|
||
setState(pv => ({ ...pv, projectsLoaded: true, projects: [...(data?.XPROJECTS || [])] }));
|
||
}
|
||
},
|
||
[executeStored, state.ident, state.projectsLoaded]
|
||
);
|
||
|
||
//Загрузка списка работ проекта
|
||
const loadProjectJobs = useCallback(
|
||
async (tasksOnly = false) => {
|
||
const data = await executeStored({
|
||
stored: "PKG_P8PANELS_PROJECTS.JB_JOBS_LIST",
|
||
args: { NIDENT: state.ident, NPRN: state.selectedProject, NINCLUDE_DEF: tasksOnly === false ? 1 : 0 },
|
||
attributeValueProcessor: (name, val) =>
|
||
name == "numb" ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val,
|
||
respArg: "COUT"
|
||
});
|
||
setState(pv => ({
|
||
...pv,
|
||
selectedProjectJobsLoaded: true,
|
||
selectedProjectGanttDef: tasksOnly === true ? { ...pv.selectedProjectGanttDef } : data.XGANTT_DEF ? { ...data.XGANTT_DEF } : {},
|
||
selectedProjectTasks: [...data.XGANTT_TASKS]
|
||
}));
|
||
},
|
||
[executeStored, state.ident, state.selectedProject]
|
||
);
|
||
|
||
//Изменение работы в графике
|
||
const modifyJob = useCallback(
|
||
async (job, dateFrom, dateTo) => {
|
||
let data = null;
|
||
try {
|
||
data = await executeStored({
|
||
stored: "PKG_P8PANELS_PROJECTS.JB_JOBS_MODIFY_PERIOD",
|
||
args: { NJB_JOBS: job, DDATE_FROM: dateFrom, DDATE_TO: dateTo }
|
||
});
|
||
if (data?.NRESOURCE_STATUS != -1) {
|
||
setState(pv => ({ ...pv, resourceStatus: data.NRESOURCE_STATUS, needSave: true }));
|
||
loadProjects(true);
|
||
}
|
||
} finally {
|
||
loadProjectJobs(true);
|
||
}
|
||
},
|
||
[executeStored, loadProjectJobs, loadProjects]
|
||
);
|
||
|
||
//Сохранение буфера балансировки в проекты
|
||
const saveProjects = useCallback(async () => {
|
||
const data = await executeStored({
|
||
stored: "PKG_P8PANELS_PROJECTS.JB_SAVE",
|
||
args: { NIDENT: state.ident },
|
||
respArg: "COUT"
|
||
});
|
||
setState(pv => ({ ...pv, needSave: false, projects: [...(data?.XPROJECTS || [])] }));
|
||
}, [executeStored, state.ident]);
|
||
|
||
//Инициализация данных балансировки
|
||
const initJobs = useCallback(async () => {
|
||
if (!state.init) {
|
||
const data = await executeStored({
|
||
stored: "PKG_P8PANELS_PROJECTS.JB_INIT",
|
||
args: {
|
||
DBEGIN: state.dateBegin ? state.dateBegin : null,
|
||
DFACT: state.dateFact ? state.dateFact : null,
|
||
NIDENT: state.ident
|
||
}
|
||
});
|
||
setState(pv => ({
|
||
...pv,
|
||
init: true,
|
||
reInit: false,
|
||
dateBegin: new Date(data.DBEGIN),
|
||
dateFact: new Date(data.DFACT),
|
||
durationMeas: data.NDURATION_MEAS,
|
||
durationMeasCode: data.SDURATION_MEAS,
|
||
labMeas: data.NLAB_MEAS,
|
||
labMeasCode: data.SLAB_MEAS,
|
||
resourceStatus: data.NRESOURCE_STATUS,
|
||
ident: data.NIDENT
|
||
}));
|
||
}
|
||
}, [state.init, state.dateBegin, state.dateFact, state.ident, executeStored]);
|
||
|
||
//Грузим список проектов при смене идентификатора процесса
|
||
useEffect(() => {
|
||
if (state.ident) loadProjects();
|
||
}, [state.ident, loadProjects]);
|
||
|
||
//При смене выбранного проекта
|
||
useEffect(() => {
|
||
if (state.selectedProject) loadProjectJobs(false);
|
||
}, [state.selectedProject, loadProjectJobs]);
|
||
|
||
//При изменении флага инициализации
|
||
useEffect(() => {
|
||
initJobs();
|
||
}, [state.init, initJobs]);
|
||
|
||
//Выбор проекта
|
||
const selectPoject = (project, projectDocRn) => {
|
||
setState(pv => ({
|
||
...pv,
|
||
selectedProject: project,
|
||
selectedProjectDocRn: projectDocRn,
|
||
selectedProjectJobsLoaded: false,
|
||
selectedProjectTasks: [],
|
||
selectedProjectGanttDef: {},
|
||
showProjectsList: false
|
||
}));
|
||
};
|
||
|
||
//Сброс выбора проекта
|
||
const unselectProject = () =>
|
||
setState(pv => ({
|
||
...pv,
|
||
selectedProjectJobsLoaded: false,
|
||
selectedProject: null,
|
||
selectedProjectDocRn: null,
|
||
selectedProjectTasks: [],
|
||
selectedProjectGanttDef: {},
|
||
showProjectsList: false
|
||
}));
|
||
|
||
//Обработка нажатия на элемент в списке проектов
|
||
const handleProjectClick = project => {
|
||
if (state.selectedProject != project.NRN) selectPoject(project.NRN, project.NPROJECT);
|
||
else unselectProject();
|
||
};
|
||
|
||
//Отработка нажатия на заголовок плана-графика
|
||
const handleTitleClick = () =>
|
||
state.selectedProjectDocRn ? pOnlineShowDocument({ unitCode: "Projects", document: state.selectedProjectDocRn }) : null;
|
||
|
||
//Обработка измненения сроков задачи в диаграмме Гантта
|
||
const handleTaskDatesChange = ({ task, start, end, isMain }) => {
|
||
if (isMain) modifyJob(task.rn, new Date(start), new Date(end));
|
||
};
|
||
|
||
//Отработка нажатия на отображения диалога параметров инициализации панели
|
||
const handleShowInitDialogClick = () => setState(pv => ({ ...pv, showInitDialog: true }));
|
||
|
||
//Отработка нажатия на "ОК" в диалоге параметров инициализации панели
|
||
const handleOKInitDialogClick = values =>
|
||
setState(pv => ({ ...pv, dateBegin: values.dateBegin, dateFact: values.dateFact, showInitDialog: false, init: false }));
|
||
|
||
//Отработка нажатия на "Отмена" в диалоге параметров инициализации панели
|
||
const handleCancelInitDialogClick = () => setState(pv => ({ ...pv, showInitDialog: false }));
|
||
|
||
//Обработка нажатия на сохранение данных в проект
|
||
const handleSaveToProjectsClick = () => saveProjects();
|
||
|
||
//Обработка нажатия на проект в таблице детализации трудоёмкости по плану-графику монитора ресурсов
|
||
const handlePlanJobsDtlProjectClick = ({ sender }) => {
|
||
setState(pv => ({ ...pv, showPeriodsList: false }));
|
||
if (state.selectedProject != sender.NJB_PRJCTS) selectPoject(sender.NJB_PRJCTS, sender.NPROJECT);
|
||
};
|
||
|
||
//Генерация содержимого
|
||
return (
|
||
<Box>
|
||
{state.showInitDialog ? (
|
||
<InitPrmsDialog
|
||
dateBegin={state.dateBegin}
|
||
dateFact={state.dateFact}
|
||
onOk={handleOKInitDialogClick}
|
||
onCancel={handleCancelInitDialogClick}
|
||
/>
|
||
) : null}
|
||
<Fab variant="extended" sx={STYLES.PROJECTS_BUTTON} onClick={() => setState(pv => ({ ...pv, showProjectsList: !pv.showProjectsList }))}>
|
||
Проекты
|
||
{state.needSave ? (
|
||
<>
|
||
|
||
<Icon sx={{ color: "orange" }}>save</Icon>
|
||
</>
|
||
) : null}
|
||
</Fab>
|
||
<Drawer
|
||
anchor={"left"}
|
||
open={state.showProjectsList}
|
||
onClose={() => setState(pv => ({ ...pv, showProjectsList: false }))}
|
||
sx={STYLES.PROJECTS_DRAWER}
|
||
>
|
||
{state.projectsLoaded ? (
|
||
<>
|
||
<InitPrmsArea
|
||
dateBegin={state.dateBegin}
|
||
dateFact={state.dateFact}
|
||
durationMeasCode={state.durationMeasCode}
|
||
labMeasCode={state.labMeasCode}
|
||
onClick={handleShowInitDialogClick}
|
||
/>
|
||
<Divider />
|
||
{state.needSave ? (
|
||
<>
|
||
<SaveChangesArea onClick={handleSaveToProjectsClick} />
|
||
<Divider />
|
||
</>
|
||
) : null}
|
||
<ProjectsList projects={state.projects} selectedProject={state.selectedProject} onClick={handleProjectClick} />
|
||
</>
|
||
) : null}
|
||
</Drawer>
|
||
<Fab variant="extended" sx={STYLES.PERIODS_BUTTON} onClick={() => setState(pv => ({ ...pv, showPeriodsList: !pv.showPeriodsList }))}>
|
||
Ресурсы
|
||
{[0, 1].includes(state.resourceStatus) ? (
|
||
<>
|
||
|
||
<Icon sx={{ color: state.resourceStatus === 0 ? "green" : "red" }}>{state.resourceStatus === 0 ? "done" : "error"}</Icon>
|
||
</>
|
||
) : null}
|
||
</Fab>
|
||
<Drawer
|
||
anchor={"right"}
|
||
open={state.showPeriodsList}
|
||
onClose={() => setState(pv => ({ ...pv, showPeriodsList: false }))}
|
||
sx={STYLES.PERIODS_DRAWER}
|
||
>
|
||
{state.ident ? <ResMon ident={state.ident} onPlanJobsDtlProjectClick={handlePlanJobsDtlProjectClick} /> : null}
|
||
</Drawer>
|
||
{state.init == true ? (
|
||
<Grid container>
|
||
<Grid item xs={12}>
|
||
{state.selectedProjectJobsLoaded ? (
|
||
<P8PGantt
|
||
{...P8P_GANTT_CONFIG_PROPS}
|
||
{...state.selectedProjectGanttDef}
|
||
containerStyle={STYLES.GANTT_CONTAINER}
|
||
titleStyle={STYLES.GANTT_TITLE}
|
||
onTitleClick={handleTitleClick}
|
||
tasks={state.selectedProjectTasks}
|
||
onTaskDatesChange={handleTaskDatesChange}
|
||
taskAttributeRenderer={taskAttributeRenderer}
|
||
/>
|
||
) : !state.selectedProject ? (
|
||
<Box pt={3}>
|
||
<InlineMsgInfo okBtn={false} text={"Укажите проект для отображения его плана-графика"} />
|
||
</Box>
|
||
) : null}
|
||
</Grid>
|
||
</Grid>
|
||
) : null}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
//----------------
|
||
//Интерфейс модуля
|
||
//----------------
|
||
|
||
export { PrjJobs };
|