Добавлена страница Оценки по ЦБ

This commit is contained in:
Александр Сироткин 2022-03-13 22:11:58 +05:00
parent 6a09ab9af3
commit afed418b56
10 changed files with 416 additions and 44 deletions

View File

@ -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<T = any> = ColumnGroupType<T> | ColumnType<T>
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, ColumnSettings>[]
export type TableContainer = TableProps<any> & {
columns: TableColumns
dataSource: any[]
tableName?: string
showSettingsChanger?: boolean
columns: TableColumns
dataSource: any[]
tableName?: string
showSettingsChanger?: boolean
}
export const Table = memo<TableContainer>(({ columns, dataSource, tableName, showSettingsChanger, ...other }) => {
const [newColumns, setNewColumns] = useState<TableColumns>([])
const [settings, setSettings] = useState<TableSettings>({})
const [newColumns, setNewColumns] = useState<TableColumns>([])
const [settings, setSettings] = useState<TableSettings>({})
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) => (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'space-between', position: 'relative', padding: '16px 0' }}>
<TableSettingsChanger columns={columns} settings={settings} onChange={onSettingsChanged}/>
<div>
{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) => (
<div className={'first-column-title'}>
<TableSettingsChanger columns={columns} settings={settings} onChange={onSettingsChanged}/>
<div>{typeof oldTitle === 'function' ? oldTitle(props) : oldTitle}</div>
</div>
</div>
)
}
return newColumns
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
)
}
return newColumns
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
return (
<RawTable
columns={newColumns}
dataSource={tryAddKeys(dataSource)}
{...other}
/>
)
return (
<RawTable
columns={newColumns}
dataSource={tryAddKeys(dataSource)}
{...other}
/>
)
})
export default Table

View File

@ -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 (
<TreeSelect
multiple
treeCheckable
showCheckedStrategy={TreeSelect.SHOW_CHILD}
treeDefaultExpandAll
treeData={wellsTree}
treeLine={{ showLeafIcon: false }}
size={'middle'}
style={{ width: '100%' }}
value={value}
onChange={onChange}
placeholder={'Выберите скважины'}
tagRender={(props) => (
<Tag {...props}>{wellLabels[props.value] ?? props.label}</Tag>
)}
disabled={wellsTree.length <= 0 || !hasPermission('WellOperation.edit')}
{...other}
/>
)
})
export default WellSelector

View File

@ -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<sub>рейсовая</sub></>), 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 (
<span className={spanClass}>
{numericRender(section?.speed)}
</span>
)
}, [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) => (
<Summary fixed={'bottom'}>
<Row>
<Cell index={0} colSpan={summaryColSpan}>
<Text>Итого:</Text>
</Cell>
{sectionTypes.map(([id, _], i) => (
<Fragment key={id ?? i}>
<Cell index={3 * i + summaryColSpan}>
<Text>{numericRender(avgRow[`section_${id}`]?.depth)}</Text>
</Cell>
<Cell index={3 * i + summaryColSpan + 1}>
<Text>{numericRender(avgRow[`section_${id}`]?.time)}</Text>
</Cell>
<Cell index={3 * i + summaryColSpan + 2}>
<Text>{numericRender(avgRow[`section_${id}`]?.speed)}</Text>
</Cell>
</Fragment>
))}
</Row>
</Summary>
), [avgRow, sectionTypes])
return (
<div className={'statistics-page'}>
<LoaderPortal show={isPageLoading}>
<h2 style={{ padding: '10px' }}>Расчёт средних значений без Цифровой буровой</h2>
<div className={'average-table'}>
<div className={'well-selector'}>
<span className={'well-selector-label'}>Выберите скважины для расчёта средних значений:</span>
<WellSelector
idWell={idWell}
value={avgWells}
style={{ flex: 100 }}
onChange={setAvgWells}
placeholder={'Ничего не выбрано'}
/>
</div>
<Table
bordered
size={'small'}
pagination={false}
columns={avgColumns}
dataSource={avgData}
scroll={scrollSettings}
loading={isAvgTableLoading}
summary={getStatisticsAvgSummary}
/>
</div>
<div className={'compare-table'}>
<div className={'well-selector'}>
<span className={'well-selector-label'}>Выберите скважины сравнения:</span>
<WellSelector
idWell={idWell}
value={cmpWells}
style={{ flex: 100 }}
onChange={setCmpWells}
placeholder={'Ничего не выбрано'}
/>
</div>
<Table
bordered
size={'small'}
pagination={false}
columns={cmpColumns}
dataSource={cmpData}
scroll={scrollSettings}
loading={isCmpTableLoading}
/>
</div>
</LoaderPortal>
</div>
)
})
export default Statistics

View File

@ -86,7 +86,7 @@ export const WellCompositeEditor = memo(({ idWell, rootPath }) => {
return (
<LoaderPortal show={showLoader}>
<Row align={'middle'} justify={'space-between'} wrap={false}>
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
<Col span={18}>
<TreeSelect
multiple

View File

@ -0,0 +1,37 @@
import { memo } from 'react'
import { Layout, Menu } from 'antd'
import { Switch, useParams } from 'react-router-dom'
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
import WellCompositeEditor from './WellCompositeEditor'
import Statistics from './Statistics'
export const Analytics = memo(({ idWell }) => {
const { tab } = useParams()
const rootPath = `/well/${idWell}/analytics`
return (
<Layout>
<Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}>
<PrivateMenuItemLink root={rootPath} key={'composite'} path={'composite'} title={'Композитная скважина'} />
<PrivateMenuItemLink root={rootPath} key={'statistics'} path={'statistics'} title={'Оценка по ЦБ'} />
</Menu>
<Layout>
<Layout.Content>
<Switch>
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
<WellCompositeEditor idWell={idWell} rootPath={`${rootPath}/composite`} />
</PrivateRoute>
<PrivateRoute path={`${rootPath}/statistics`}>
<Statistics idWell={idWell} />
</PrivateRoute>
<PrivateDefaultRoute urls={[`${rootPath}/composite`]}/>
</Switch>
</Layout.Content>
</Layout>
</Layout>
)
})
export default Analytics

View File

@ -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(() => {
<PrivateMenuItem.Link root={rootPath} key={'telemetry'} path={'telemetry'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
<PrivateMenuItem.Link root={rootPath} key={'message'} path={'message'} icon={<AlertOutlined/>} title={'Сообщения'} />
<PrivateMenuItem.Link root={rootPath} key={'report'} path={'report'} icon={<FilePdfOutlined />} title={'Рапорт'} />
<PrivateMenuItem.Link root={rootPath} key={'composite'} path={'composite'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} />
<PrivateMenuItem.Link root={rootPath} key={'analytics'} path={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} />
<PrivateMenuItem.Link root={rootPath} key={'operations'} path={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} />
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
{/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */}
@ -59,8 +59,8 @@ export const Well = memo(() => {
<PrivateRoute path={`${rootPath}/report`}>
<Report idWell={idWell} />
</PrivateRoute>
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
<WellCompositeEditor idWell={idWell} rootPath={`${rootPath}/composite`}/>
<PrivateRoute path={`${rootPath}/analytics/:tab?`}>
<Analytics idWell={idWell} rootPath={`${rootPath}/analytics`}/>
</PrivateRoute>
<PrivateRoute path={`${rootPath}/operations/:tab?`}>
<WellOperations idWell={idWell} />
@ -84,7 +84,7 @@ export const Well = memo(() => {
`${rootPath}/telemetry`,
`${rootPath}/message`,
`${rootPath}/report`,
`${rootPath}/composite`,
`${rootPath}/analytics`,
`${rootPath}/operations`,
`${rootPath}/archive`,
`${rootPath}/telemetryAnalysis`,

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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'],