forked from ddrilling/asb_cloud_front
Переработана страница TVD и метод getOperations
This commit is contained in:
parent
b15a3b4363
commit
a55d0c9b42
@ -1,5 +1,7 @@
|
||||
import { Switch } from 'antd'
|
||||
import { memo, useState, useRef, useEffect } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { memo, useState, useRef, useEffect, useCallback } from 'react'
|
||||
import { Switch, Button, InputNumber, Descriptions } from 'antd'
|
||||
import { DoubleLeftOutlined, DoubleRightOutlined, FilterOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
@ -16,9 +18,27 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { makeDateColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
|
||||
import { formatDate, fractionalSum } from '@utils/datetime'
|
||||
import { getOperations } from '@utils/functions'
|
||||
|
||||
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
|
||||
import '@styles/index.css'
|
||||
import '@styles/tvd.less'
|
||||
|
||||
const { Item } = Descriptions
|
||||
|
||||
Chart.register(
|
||||
TimeScale,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Legend,
|
||||
ChartDataLabels,
|
||||
zoomPlugin
|
||||
)
|
||||
|
||||
const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-'
|
||||
|
||||
const scaleTypes = {
|
||||
day: {
|
||||
@ -51,10 +71,11 @@ const scaleTypes = {
|
||||
|
||||
const defaultOptions = {
|
||||
responsive: true,
|
||||
aspectRatio: 2.6,
|
||||
maintainAspectRatio: false,
|
||||
aspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
mode: 'nearest',
|
||||
},
|
||||
scales: {
|
||||
x: scaleTypes.day,
|
||||
@ -89,14 +110,29 @@ const defaultOptions = {
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
label: (tooltipItem) => tooltipItem.yLabel,
|
||||
},
|
||||
position: 'nearest',
|
||||
callbacks: {
|
||||
title: (items) => [
|
||||
`Дата: ${formatDate(items[0].raw?.date) ?? '-'}`,
|
||||
`День с начала бурения: ${parseInt(items[0].raw?.day)}`,
|
||||
],
|
||||
afterTitle: (items) => `Глубина: ${numericRender(items[0].raw?.depth)}`,
|
||||
label: (item) => [
|
||||
item.raw.wellSectionTypeName + ': ' + item.raw.categoryName,
|
||||
`Длительность (ч): ${item.raw.nptHours}`
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const nptTableColumns = [
|
||||
makeTextColumn('Конструкция секции', 'wellSectionTypeName'),
|
||||
makeNumericColumn('Глубина', 'depth'),
|
||||
makeDateColumn('Дата начала', 'date'),
|
||||
makeNumericColumn('Длительность (ч)', 'durationHours'),
|
||||
]
|
||||
|
||||
const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
|
||||
label,
|
||||
data,
|
||||
@ -106,38 +142,101 @@ const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
|
||||
borderDash,
|
||||
})
|
||||
|
||||
const calcEndDate = (saubData) => {
|
||||
if (!Array.isArray(saubData) || saubData.length <= 0) return [null, null]
|
||||
const lastElm = saubData.at(-1)
|
||||
return [saubData[0]?.date, fractionalSum(lastElm?.date, lastElm?.nptHours, 'hour')]
|
||||
}
|
||||
|
||||
const printDate = (date) => formatDate(date) ?? '-'
|
||||
|
||||
export const Tvd = memo(({ idWell, title }) => {
|
||||
const [operations, setOperations] = useState([])
|
||||
const [operations, setOperations] = useState({})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isTableLoading, setIsTableLoading] = useState(false)
|
||||
const [xLabel, setXLabel] = useState('day')
|
||||
const [chart, setChart] = useState()
|
||||
const [npt, setNPT] = useState([])
|
||||
const [filteredNPT, setFilteredNPT] = useState([])
|
||||
const [filterValue, setFilterValue] = useState(null)
|
||||
const [tableVisible, setTableVisible] = useState(true)
|
||||
const [additionalData, setAdditionalData] = useState({})
|
||||
|
||||
const chartRef = useRef(null)
|
||||
const history = useHistory()
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const operations = await getOperations(idWell)
|
||||
setOperations(operations)
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось загрузить операции по скважине "${idWell}"`,
|
||||
'Получение списка опервций по скважине'
|
||||
)
|
||||
}, [idWell])
|
||||
const onPointClick = useCallback((e) => {
|
||||
const points = e?.chart?.tooltip?.dataPoints
|
||||
if (!points || !(points.length > 0)) return
|
||||
|
||||
const datasetId = points.find((p) => p.datasetIndex !== 1)?.datasetIndex
|
||||
if (typeof datasetId === 'undefined') return
|
||||
|
||||
const datasetName = datasetId === 2 ? 'plan' : 'fact'
|
||||
const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id)
|
||||
const url = `/well/${idWell}/operations/${datasetName}/?selectedId=${ids.join(',')}`
|
||||
history.push(url)
|
||||
}, [idWell, history])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const operations = await getOperations(idWell)
|
||||
setNPT(operations.fact.filter((row) => row.isNPT) ?? [])
|
||||
setOperations(operations)
|
||||
|
||||
const [factStartDate, factEndDate] = calcEndDate(operations.fact)
|
||||
const [planStartDate, planEndDate] = calcEndDate(operations.plan)
|
||||
const [predictStartDate, predictEndDate] = calcEndDate(operations.predict)
|
||||
|
||||
const last = predictEndDate ?? factEndDate
|
||||
setAdditionalData({
|
||||
lag: (+new Date(last) - +new Date(planEndDate)) / 86400_000,
|
||||
endDate: last,
|
||||
factStartDate,
|
||||
factEndDate,
|
||||
planStartDate,
|
||||
planEndDate,
|
||||
predictStartDate,
|
||||
predictEndDate,
|
||||
})
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось загрузить операции по скважине "${idWell}"`,
|
||||
'Получение списка опервций по скважине'
|
||||
), [idWell])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)),
|
||||
setIsTableLoading,
|
||||
'Не удалось отфильтровать НПВ по времени'
|
||||
), [npt, filterValue])
|
||||
|
||||
useEffect(() => {
|
||||
const npt = []
|
||||
operations?.fact?.forEach((row) => {
|
||||
if (row?.isNPT !== false) return
|
||||
const nptH = +(row.nptHours ?? 0)
|
||||
npt.push({
|
||||
...row,
|
||||
day: row.day - nptH / 24,
|
||||
date: fractionalSum(row.date, -nptH, 'hour'),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const data = {
|
||||
datasets: [
|
||||
makeDataset(operations?.fact, 'Факт', '#0A0'),
|
||||
makeDataset(operations?.fact, 'Факт', '#0A0', 3),
|
||||
makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]),
|
||||
makeDataset(operations?.plan, 'План', '#C004', 4),
|
||||
makeDataset(operations?.plan, 'План', '#F00', 3),
|
||||
makeDataset(npt, 'Факт без НПВ', '#00F', 3)
|
||||
]
|
||||
}
|
||||
|
||||
if (chartRef.current && !chart) {
|
||||
const thisOptions = {}
|
||||
Object.assign(thisOptions, defaultOptions)
|
||||
thisOptions.onClick = onPointClick
|
||||
thisOptions.scales.x = scaleTypes[xLabel]
|
||||
thisOptions.parsing.xAxisKey = xLabel
|
||||
|
||||
@ -155,27 +254,85 @@ export const Tvd = memo(({ idWell, title }) => {
|
||||
chart.options.scales.x = scaleTypes[xLabel]
|
||||
chart.options.parsing.xAxisKey = xLabel
|
||||
chart.update()
|
||||
// Обнуление ширины необходимо для уменьшения размена при resize после появления элементов
|
||||
chart.canvas.parentNode.style.width = '0'
|
||||
chart.resize()
|
||||
}
|
||||
}, [chart, operations, xLabel])
|
||||
}, [chart, operations, xLabel, onPointClick])
|
||||
|
||||
const toogleTable = useCallback(() => {
|
||||
setOperations((pre) => ({ ...pre }))
|
||||
setTableVisible((pre) => !pre)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'container'}>
|
||||
<div>
|
||||
{title || (
|
||||
<h2 className={'mt-20px'}>График Глубина-день</h2>
|
||||
)}
|
||||
<LoaderPortal show={isLoading}>
|
||||
<canvas ref={chartRef} />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Switch
|
||||
checkedChildren={'Дата'}
|
||||
unCheckedChildren={'Дни со старта'}
|
||||
loading={isLoading}
|
||||
onChange={(checked) => setXLabel(checked ? 'date' : 'day')}
|
||||
/>
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
<div className={'container tvd-page'}>
|
||||
<div className={'tvd-top'}>
|
||||
<h2>{title || 'График Глубина-день'}</h2>
|
||||
<div>
|
||||
<Switch
|
||||
checkedChildren={'Дата'}
|
||||
unCheckedChildren={'Дни со старта'}
|
||||
loading={isLoading}
|
||||
onChange={(checked) => setXLabel(checked ? 'date' : 'day')}
|
||||
style={{ marginRight: '20px' }}
|
||||
/>
|
||||
<Button
|
||||
icon={tableVisible ? <DoubleLeftOutlined /> : <DoubleRightOutlined />}
|
||||
onClick={toogleTable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LoaderPortal show={isLoading} style={{ flex: '1'}}>
|
||||
<div className={'tvd-main'}>
|
||||
<div className={'tvd-left'}>
|
||||
<div className={'tvd-tr-table'} onMouseEnter={(e) => console.log(e)}>
|
||||
<Descriptions bordered column={1} size={'small'} style={{ backgroundColor: 'white' }}>
|
||||
<Item label={'Дата'}>{printDate(additionalData.endDate)}</Item>
|
||||
<Item label={'Отставание (сут)'}>{numericRender(additionalData.lag)}</Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
<canvas ref={chartRef} />
|
||||
<div className={'tvd-bl-table'} style={{ bottom: xLabel === 'day' ? '35px' : '85px' }}>
|
||||
<Descriptions bordered column={1} size={'small'} style={{ backgroundColor: 'white' }}>
|
||||
<Item label={'Отставание (сут)'}>{numericRender(additionalData.lag)}</Item>
|
||||
<Item label={'Начало цикла (план)'}>{printDate(additionalData.planStartDate)}</Item>
|
||||
<Item label={'Начало цикла (факт)'}>{printDate(additionalData.factStartDate)}</Item>
|
||||
<Item label={'Окончание цикла (план)'}>{printDate(additionalData.planEndDate)}</Item>
|
||||
<Item label={'Окончание цикла (факт)'}>{printDate(additionalData.factEndDate)}</Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
{tableVisible && (
|
||||
<div className={'tvd-right'}>
|
||||
<div className={'tvd-npt-filter'}>
|
||||
<FilterOutlined />
|
||||
<span>Фильтр время НПВ ≥</span>
|
||||
<InputNumber
|
||||
step={0.5}
|
||||
max={10000}
|
||||
min={0}
|
||||
defaultValue={0}
|
||||
onChange={(value) => setFilterValue(value ?? 0)}
|
||||
value={filterValue}
|
||||
/>
|
||||
<span>ч.</span>
|
||||
</div>
|
||||
<LoaderPortal show={isTableLoading}>
|
||||
<Table
|
||||
bordered
|
||||
size={'small'}
|
||||
dataSource={filteredNPT}
|
||||
columns={nptTableColumns}
|
||||
pagination={false}
|
||||
tableName={'tvd_npt'}
|
||||
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
65
src/styles/tvd.less
Normal file
65
src/styles/tvd.less
Normal file
@ -0,0 +1,65 @@
|
||||
.tvd-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.tvd-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tvd-main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
.tvd-left {
|
||||
position: relative;
|
||||
flex: 2;
|
||||
|
||||
> div {
|
||||
position: absolute;
|
||||
//pointer-events: none;
|
||||
transition: all .25s ease-out;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.tvd-tr-table {
|
||||
right: 15px;
|
||||
top: 38px;
|
||||
}
|
||||
|
||||
&.tvd-bl-table {
|
||||
bottom: 35px;
|
||||
left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tvd-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding-left: 10px;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.tvd-npt-filter {
|
||||
margin-bottom: 10px;
|
||||
|
||||
span {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,28 +7,37 @@ export const getPrecision = (number: number) => Number.isFinite(number) ? number
|
||||
|
||||
export type KeyType = number | string
|
||||
|
||||
export type SaubData = {
|
||||
key?: number
|
||||
depth?: number
|
||||
date?: string
|
||||
day?: number
|
||||
export const nwtIdCategory = 1043
|
||||
|
||||
export type SaubData = WellOperationDto & {
|
||||
isNPT?: boolean // Относится ли операция к НПВ
|
||||
key?: number // ID операции
|
||||
depth?: number // Глубина
|
||||
date?: string // Дата
|
||||
nptHours?: number // Колличество часов НПВ с начала бурения до текущего момента
|
||||
}
|
||||
|
||||
export const getOperations = async (idWell: number): Promise<{
|
||||
operations?: WellOperationDtoPlanFactPredictBase[],
|
||||
plan?: SaubData[]
|
||||
fact?: SaubData[]
|
||||
predict?: SaubData[]
|
||||
operations: WellOperationDtoPlanFactPredictBase[],
|
||||
plan: SaubData[]
|
||||
fact: SaubData[]
|
||||
predict: SaubData[]
|
||||
}> => {
|
||||
const ops = await OperationStatService.getTvd(idWell)
|
||||
|
||||
if (!ops) return {}
|
||||
if (!ops) return {
|
||||
operations: [],
|
||||
plan: [],
|
||||
fact: [],
|
||||
predict: [],
|
||||
}
|
||||
|
||||
const convert = (operation?: WellOperationDto): SaubData => ({
|
||||
...operation,
|
||||
key: operation?.id,
|
||||
depth: operation?.depthStart,
|
||||
date: operation?.dateStart,
|
||||
day: operation?.day,
|
||||
isNPT: operation?.idCategory === nwtIdCategory,
|
||||
})
|
||||
|
||||
const planData = ops
|
||||
|
Loading…
Reference in New Issue
Block a user