From afed418b56b278a0a7a63a197c8aba59e43782b5 Mon Sep 17 00:00:00 2001 From: goodm2ice Date: Sun, 13 Mar 2022 22:11:58 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B0?= =?UTF-8?q?=20=D0=9E=D1=86=D0=B5=D0=BD=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=A6?= =?UTF-8?q?=D0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Table/Table.tsx | 70 +++--- src/components/WellSelector.jsx | 77 +++++++ src/pages/Analytics/Statistics.jsx | 218 ++++++++++++++++++ .../WellCompositeSections.jsx | 0 .../WellCompositeEditor/index.jsx | 2 +- src/pages/Analytics/index.jsx | 37 +++ src/pages/Well.jsx | 10 +- src/styles/index.css | 12 + src/styles/statistics.less | 25 ++ src/utils/permissions.ts | 9 +- 10 files changed, 416 insertions(+), 44 deletions(-) create mode 100644 src/components/WellSelector.jsx create mode 100644 src/pages/Analytics/Statistics.jsx rename src/pages/{ => Analytics}/WellCompositeEditor/WellCompositeSections.jsx (100%) rename src/pages/{ => Analytics}/WellCompositeEditor/index.jsx (99%) create mode 100644 src/pages/Analytics/index.jsx create mode 100644 src/styles/statistics.less diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 0283a8d..17116cb 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,6 +1,6 @@ -import { memo, useCallback, useEffect, useState, ReactNode } from 'react' -import { Table as RawTable, TableProps } from 'antd' +import { memo, useCallback, useEffect, useState } from 'react' import { ColumnGroupType, ColumnType } from 'antd/lib/table' +import { Table as RawTable, TableProps } from 'antd' import { OmitExtends } from '@utils' import { getTableSettings, setTableSettings } from '@utils/storage' @@ -9,50 +9,50 @@ import { applySettings, ColumnSettings, TableSettings } from '@utils/table_setti import TableSettingsChanger from './TableSettingsChanger' import { tryAddKeys } from './EditableTable' +import '@styles/index.css' + export type BaseTableColumn = ColumnGroupType | ColumnType export type TableColumns = OmitExtends, ColumnSettings>[] export type TableContainer = TableProps & { - columns: TableColumns - dataSource: any[] - tableName?: string - showSettingsChanger?: boolean + columns: TableColumns + dataSource: any[] + tableName?: string + showSettingsChanger?: boolean } export const Table = memo(({ columns, dataSource, tableName, showSettingsChanger, ...other }) => { - const [newColumns, setNewColumns] = useState([]) - const [settings, setSettings] = useState({}) + const [newColumns, setNewColumns] = useState([]) + const [settings, setSettings] = useState({}) - const onSettingsChanged = useCallback((settings?: TableSettings | null) => { - if (tableName) - setTableSettings(tableName, settings) - setSettings(settings ?? {}) - }, [tableName]) + const onSettingsChanged = useCallback((settings?: TableSettings | null) => { + if (tableName) + setTableSettings(tableName, settings) + setSettings(settings ?? {}) + }, [tableName]) - useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) - useEffect(() => setNewColumns(() => { - const newColumns = applySettings(columns, settings) - if (tableName && showSettingsChanger) { - const oldTitle = newColumns[0].title - newColumns[0].title = (props) => ( -
- -
- {typeof oldTitle === 'function' ? oldTitle(props) : oldTitle} + useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) + useEffect(() => setNewColumns(() => { + const newColumns = applySettings(columns, settings) + if (tableName && showSettingsChanger) { + const oldTitle = newColumns[0].title + newColumns[0].title = (props) => ( +
+ +
{typeof oldTitle === 'function' ? oldTitle(props) : oldTitle}
-
- ) - } - return newColumns - }), [settings, columns, onSettingsChanged, showSettingsChanger, tableName]) + ) + } + return newColumns + }), [settings, columns, onSettingsChanged, showSettingsChanger, tableName]) - return ( - - ) + return ( + + ) }) export default Table diff --git a/src/components/WellSelector.jsx b/src/components/WellSelector.jsx new file mode 100644 index 0000000..c88a6c0 --- /dev/null +++ b/src/components/WellSelector.jsx @@ -0,0 +1,77 @@ +import { Tag, TreeSelect } from 'antd' +import { memo, useEffect, useState } from 'react' + +import { invokeWebApiWrapperAsync } from '@components/factory' +import { hasPermission } from '@utils/permissions' +import { DepositService } from '@api' + +export const getTreeData = async () => { + const deposits = await DepositService.getDeposits() + const wellsTree = deposits.map((deposit, dIdx) => ({ + title: deposit.caption, + key: `0-${dIdx}`, + value: `0-${dIdx}`, + children: deposit.clusters.map((cluster, cIdx) => ({ + title: cluster.caption, + key: `0-${dIdx}-${cIdx}`, + value: `0-${dIdx}-${cIdx}`, + children: cluster.wells.map(well => ({ + title: well.caption, + key: well.id, + value: well.id, + })), + })), + })) + + return wellsTree +} + +export const getTreeLabels = (treeData) => { + const labels = {} + treeData.forEach((deposit) => + deposit?.children?.forEach((cluster) => + cluster?.children?.forEach((well) => { + labels[well.value] = `${deposit.title}.${cluster.title}.${well.title}` + }))) + return labels +} + +export const WellSelector = memo(({ idWell, value, onChange, treeData, treeLabels, ...other }) => { + const [wellsTree, setWellsTree] = useState([]) + const [wellLabels, setWellLabels] = useState([]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const wellsTree = treeData ?? await getTreeData() + const labels = treeLabels ?? getTreeLabels(wellsTree) + setWellsTree(wellsTree) + setWellLabels(labels) + }, + null, + 'Не удалось загрузить список скважин', + 'Получение списка скважин' + ), [idWell, treeData]) + + return ( + ( + {wellLabels[props.value] ?? props.label} + )} + disabled={wellsTree.length <= 0 || !hasPermission('WellOperation.edit')} + {...other} + /> + ) +}) + +export default WellSelector diff --git a/src/pages/Analytics/Statistics.jsx b/src/pages/Analytics/Statistics.jsx new file mode 100644 index 0000000..363da55 --- /dev/null +++ b/src/pages/Analytics/Statistics.jsx @@ -0,0 +1,218 @@ +import { Table as RawTable, Typography } from 'antd' +import { Fragment, memo, useCallback, useEffect, useState } from 'react' + +import LoaderPortal from '@components/LoaderPortal' +import { getTreeData, getTreeLabels, WellSelector } from '@components/WellSelector' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' +import { OperationStatService, WellOperationService } from '@api' +import { arrayOrDefault } from '@utils' + +import '@styles/index.css' +import '@styles/statistics.less' + +const { Text } = Typography +const { Summary } = RawTable +const { Cell, Row } = Summary + +const numericRender = makeNumericRender() +const speedNumericRender = (section) => numericRender(section?.speed) + +export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [ + makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100), + makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100), + makeNumericColumn((<>Vрейсовая), key, null, null, speedRender ?? speedNumericRender, 100), +]) + +export const defaultColumns = [ + //makeTextColumn('Куст', 'cluster', null, null, null, { fixed: 'left', width: 100 }), + makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }), +] + +const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: 200 } +const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2 + +const getWellData = async (wellsList) => { + const stats = arrayOrDefault(await OperationStatService.getWellsStat(wellsList)) + const wellData = stats.map((well) => { + const stat = { + // cluster: null, + caption: well.caption, + } + + well.sections?.forEach(({ id, fact }) => { + if (!fact) return + stat[`section_${id}`] = { + time: (+new Date(fact.end) - +new Date(fact.start)) / 3600_000, + depth: fact.wellDepthEnd - fact.wellDepthStart, + speed: fact.routeSpeed, + } + }) + + return stat + }) + + return wellData +} + +export const Statistics = memo(({ idWell }) => { + const [sectionTypes, setSectionTypes] = useState([]) + const [avgColumns, setAvgColumns] = useState(defaultColumns) + const [cmpColumns, setCmpColumns] = useState(defaultColumns) + const [isAvgTableLoading, setIsAvgTableLoading] = useState(false) + const [isCmpTableLoading, setIsCmpTableLoading] = useState(false) + const [isPageLoading, setIsPageLoading] = useState(false) + const [avgWells, setAvgWells] = useState([]) + const [cmpWells, setCmpWells] = useState([]) + const [filteredCmpWells, setFilteredCmpWells] = useState([]) + const [avgData, setAvgData] = useState([]) + const [cmpData, setCmpData] = useState([]) + const [avgRow, setAvgRow] = useState({}) + + const cmpSpeedRender = useCallback((key) => (section) => { + let spanClass = '' + // Дополнительная проверка на "null" необходима, чтобы значение "0" не стало исключением + if ((avgRow[key]?.speed ?? null) !== null && (section?.speed ?? null) !== null) { + const avgSpeed = avgRow[key].speed - section.speed + if (avgSpeed < 0) + spanClass = 'high-efficienty' + else if (avgSpeed > 0) + spanClass = 'low-efficienty' + } + + return ( + + {numericRender(section?.speed)} + + ) + }, [avgRow]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const types = await WellOperationService.getSectionTypes(idWell) + setSectionTypes(Object.entries(types)) + setAvgColumns([ + ...defaultColumns, + ...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`)), + ]) + setCmpColumns([ + ...defaultColumns, + ...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`, { + speedRender: cmpSpeedRender(`section_${id}`) + })) + ]) + }, + setIsPageLoading, + `Не удалось получить типы секции`, + `Получение списка возможных секций`, + ), [idWell, cmpSpeedRender]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const avgData = await getWellData(avgWells) + setAvgData(avgData) + + const avgRow = {} + + avgData.forEach((row) => row && Object.keys(row).forEach((key) => { + if (!key.startsWith('section_')) return + if (!avgRow[key]) avgRow[key] = { depth: 0, time: 0, speed: 0, count: 0 } + avgRow[key].depth += row[key].depth ?? 0 + avgRow[key].time += row[key].time ?? 0 + avgRow[key].speed += row[key].speed ?? 0 + avgRow[key].count++ + })) + + Object.values(avgRow).forEach((section) => section.speed /= section.count) + + setAvgRow(avgRow) + }, + setIsAvgTableLoading, + 'Не удалось загрузить данные для расчёта средних значений', + ), [avgWells]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const cmpData = await getWellData(cmpWells) + setCmpData(cmpData) + }, + setIsCmpTableLoading, + 'Не удалось получить скважины для сравнения', + ), [cmpWells]) + + const getStatisticsAvgSummary = useCallback((data) => ( + + + + Итого: + + {sectionTypes.map(([id, _], i) => ( + + + {numericRender(avgRow[`section_${id}`]?.depth)} + + + {numericRender(avgRow[`section_${id}`]?.time)} + + + {numericRender(avgRow[`section_${id}`]?.speed)} + + + ))} + + + ), [avgRow, sectionTypes]) + + return ( +
+ +

Расчёт средних значений без Цифровой буровой

+
+
+ Выберите скважины для расчёта средних значений: + +
+ + +
+
+ Выберите скважины сравнения: + +
+
+ + + + ) +}) + +export default Statistics diff --git a/src/pages/WellCompositeEditor/WellCompositeSections.jsx b/src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx similarity index 100% rename from src/pages/WellCompositeEditor/WellCompositeSections.jsx rename to src/pages/Analytics/WellCompositeEditor/WellCompositeSections.jsx diff --git a/src/pages/WellCompositeEditor/index.jsx b/src/pages/Analytics/WellCompositeEditor/index.jsx similarity index 99% rename from src/pages/WellCompositeEditor/index.jsx rename to src/pages/Analytics/WellCompositeEditor/index.jsx index 5fba617..0061ccc 100644 --- a/src/pages/WellCompositeEditor/index.jsx +++ b/src/pages/Analytics/WellCompositeEditor/index.jsx @@ -86,7 +86,7 @@ export const WellCompositeEditor = memo(({ idWell, rootPath }) => { return ( - + { + const { tab } = useParams() + const rootPath = `/well/${idWell}/analytics` + + return ( + + + + + + + + + + + + + + + + + + + + ) +}) + +export default Analytics diff --git a/src/pages/Well.jsx b/src/pages/Well.jsx index 28bfddf..fe1e40b 100644 --- a/src/pages/Well.jsx +++ b/src/pages/Well.jsx @@ -17,12 +17,12 @@ import Report from './Report' import Archive from './Archive' import Measure from './Measure' import Messages from './Messages' +import Analytics from './Analytics' import Documents from './Documents' import TelemetryView from './TelemetryView' import WellOperations from './WellOperations' import DrillingProgram from './DrillingProgram' import TelemetryAnalysis from './TelemetryAnalysis' -import WellCompositeEditor from './WellCompositeEditor' import '@styles/index.css' @@ -38,7 +38,7 @@ export const Well = memo(() => { } title={'Мониторинг'}/> } title={'Сообщения'} /> } title={'Рапорт'} /> - } title={'Аналитика'} /> + } title={'Аналитика'} /> } title={'Операции по скважине'} /> } title={'Архив'} /> {/* } title={'Операции по телеметрии'} /> */} @@ -59,8 +59,8 @@ export const Well = memo(() => { - - + + @@ -84,7 +84,7 @@ export const Well = memo(() => { `${rootPath}/telemetry`, `${rootPath}/message`, `${rootPath}/report`, - `${rootPath}/composite`, + `${rootPath}/analytics`, `${rootPath}/operations`, `${rootPath}/archive`, `${rootPath}/telemetryAnalysis`, diff --git a/src/styles/index.css b/src/styles/index.css index fe08c79..c87c9bc 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -75,6 +75,10 @@ body { height: calc(100vh - 64px); } +.p-10 { + padding: 10px; +} + .vertical-align-center { vertical-align: center; } @@ -115,3 +119,11 @@ code { margin: auto; } +.first-column-title { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; + position: relative; + padding: 16px 0; +} diff --git a/src/styles/statistics.less b/src/styles/statistics.less new file mode 100644 index 0000000..90565d7 --- /dev/null +++ b/src/styles/statistics.less @@ -0,0 +1,25 @@ +.statistics-page { + background-color: white; + + .well-selector { + display: flex; + padding: 10px 0 10px 10px; + align-items: center; + + > .well-selector-label { + flex: auto; + } + } + + .compare-table { + .high-efficienty { + color: limegreen; + font-weight: 600; + } + + .low-efficienty { + color: red; + font-weight: 600; + } + } +} diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index a080a23..9d34ad8 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -93,9 +93,12 @@ export const requirements: PermissionRecord = { telemetry: ['Deposit.get', 'DrillFlowChart.get', 'TelemetryDataSaub.get', 'TelemetryDataSpin.get'], message: ['Deposit.get', 'TelemetryDataSaub.get'], report: ['Deposit.get', 'Report.get'], - composite: { - wells: ['Deposit.get', 'OperationStat.get', 'WellComposite.get'], - sections: ['Deposit.get', 'OperationStat.get', 'WellComposite.get', 'DrillParams.get'], + analytics: { + composite: { + wells: ['Deposit.get', 'OperationStat.get', 'WellComposite.get'], + sections: ['Deposit.get', 'OperationStat.get', 'WellComposite.get', 'DrillParams.get'], + }, + statistics: ['Deposit.get', 'WellOperation.get'], }, operations: { tvd: ['Deposit.get', 'OperationStat.get'],