forked from ddrilling/asb_cloud_front
Tvd перенесена в отдельную директорию и разделена на файлы
This commit is contained in:
parent
98f20a4ef8
commit
1f5cc759be
@ -1,355 +0,0 @@
|
|||||||
import { useHistory } from 'react-router-dom'
|
|
||||||
import { memo, useState, useRef, useEffect, useCallback } from 'react'
|
|
||||||
import { Switch, Button, InputNumber, Descriptions } from 'antd'
|
|
||||||
import { DoubleLeftOutlined, DoubleRightOutlined, ExportOutlined, FilterOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
TimeScale,
|
|
||||||
LinearScale,
|
|
||||||
Legend,
|
|
||||||
LineController,
|
|
||||||
PointElement,
|
|
||||||
LineElement
|
|
||||||
} from 'chart.js'
|
|
||||||
import 'chartjs-adapter-moment'
|
|
||||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { download, invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { makeDateColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
|
|
||||||
import { formatDate, fractionalSum } from '@utils/datetime'
|
|
||||||
import { getOperations } from '@utils/functions'
|
|
||||||
|
|
||||||
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: {
|
|
||||||
min: 0,
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
title: { display: false, text: '' },
|
|
||||||
ticks: { stepSize: 1 }
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
display: true,
|
|
||||||
title: { display: true },
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'hour',
|
|
||||||
displayFormats: { 'hour': 'MM.DD' }
|
|
||||||
},
|
|
||||||
grid: { drawTicks: true },
|
|
||||||
ticks: {
|
|
||||||
stepSize: 3,
|
|
||||||
major: { enabled: true },
|
|
||||||
z: 1,
|
|
||||||
display: true,
|
|
||||||
textStrokeColor: '#fff',
|
|
||||||
textStrokeWidth: 2,
|
|
||||||
color: '#000',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
aspectRatio: false,
|
|
||||||
interaction: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'nearest',
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: scaleTypes.day,
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
position: 'top',
|
|
||||||
reverse: true,
|
|
||||||
display: true,
|
|
||||||
title: {
|
|
||||||
display: false,
|
|
||||||
text: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parsing: {
|
|
||||||
xAxisKey: 'day',
|
|
||||||
yAxisKey: 'depth',
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 1.7,
|
|
||||||
//backgroundColor:'#aaa',
|
|
||||||
//pointStyle:'triangle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: true,
|
|
||||||
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,
|
|
||||||
backgroundColor: color,
|
|
||||||
borderColor: color,
|
|
||||||
borderWidth,
|
|
||||||
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 [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [isTableLoading, setIsTableLoading] = useState(false)
|
|
||||||
const [isFileExporting, setIsFileExporting] = 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()
|
|
||||||
|
|
||||||
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', 3),
|
|
||||||
makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]),
|
|
||||||
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
|
|
||||||
|
|
||||||
const newChart = new Chart(chartRef.current, {
|
|
||||||
type: 'line',
|
|
||||||
plugins: [ChartDataLabels],
|
|
||||||
options: thisOptions,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
setChart(newChart)
|
|
||||||
|
|
||||||
return () => chart?.destroy()
|
|
||||||
} else {
|
|
||||||
chart.data = data
|
|
||||||
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, onPointClick])
|
|
||||||
|
|
||||||
const toogleTable = useCallback(() => {
|
|
||||||
setOperations((pre) => ({ ...pre }))
|
|
||||||
setTableVisible((pre) => !pre)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onExport = useCallback(() => invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
await download(`/api/well/${idWell}/wellOperations/scheduleReport`)
|
|
||||||
},
|
|
||||||
setIsFileExporting,
|
|
||||||
'Не удалось загрузить файл'
|
|
||||||
), [idWell])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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={<ExportOutlined />}
|
|
||||||
loading={isFileExporting}
|
|
||||||
onClick={onExport}
|
|
||||||
style={{ marginRight: '5px' }}
|
|
||||||
>Экспорт</Button>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Tvd
|
|
67
src/pages/WellOperations/Tvd/AdditionalTables.jsx
Normal file
67
src/pages/WellOperations/Tvd/AdditionalTables.jsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Descriptions } from 'antd'
|
||||||
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { makeNumericRender } from '@components/Table'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { formatDate, fractionalSum } from '@utils/datetime'
|
||||||
|
|
||||||
|
import '@styles/tvd.less'
|
||||||
|
|
||||||
|
const { Item } = Descriptions
|
||||||
|
|
||||||
|
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 numericRender = makeNumericRender(2)
|
||||||
|
const printDate = (date) => formatDate(date) ?? '-'
|
||||||
|
|
||||||
|
export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
|
||||||
|
const [additionalData, setAdditionalData] = useState({})
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
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,
|
||||||
|
'Не удалось высчитать дополнительные данные'
|
||||||
|
), [operations, setIsLoading])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'tvd-tr-table'}>
|
||||||
|
<Descriptions bordered column={1} size={'small'} style={{ backgroundColor: 'white' }}>
|
||||||
|
<Item label={'Дата завершения'}>{printDate(additionalData.endDate)}</Item>
|
||||||
|
<Item label={'Отставание (сут)'}>{numericRender(additionalData.lag)}</Item>
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AdditionalTables
|
29
src/pages/WellOperations/Tvd/NetGraphExport.jsx
Normal file
29
src/pages/WellOperations/Tvd/NetGraphExport.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { memo, useCallback, useState } from 'react'
|
||||||
|
import { ExportOutlined } from '@ant-design/icons'
|
||||||
|
import { Button } from 'antd'
|
||||||
|
|
||||||
|
import { download, invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
|
||||||
|
export const NetGraphExport = memo(({ idWell, ...other }) => {
|
||||||
|
const [isFileExporting, setIsFileExporting] = useState(false)
|
||||||
|
|
||||||
|
const onExport = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => await download(`/api/well/${idWell}/wellOperations/scheduleReport`),
|
||||||
|
setIsFileExporting,
|
||||||
|
'Не удалось загрузить файл'
|
||||||
|
), [idWell])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
loading={isFileExporting}
|
||||||
|
onClick={onExport}
|
||||||
|
style={{ marginRight: '5px' }}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
Сетевой график
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default NetGraphExport
|
66
src/pages/WellOperations/Tvd/NptTable.jsx
Normal file
66
src/pages/WellOperations/Tvd/NptTable.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
import { InputNumber } from 'antd'
|
||||||
|
import { FilterOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { makeDateColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
makeTextColumn('Конструкция секции', 'wellSectionTypeName', null, null, null, { width: 140 }),
|
||||||
|
makeNumericColumn('Глубина', 'depth', null, null, null, 80),
|
||||||
|
makeDateColumn('Дата начала', 'date', false, undefined, { width: 90 }),
|
||||||
|
makeNumericColumn('Длительность (ч)', 'durationHours', null, null, null, 120),
|
||||||
|
makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null),
|
||||||
|
makeTextColumn('Комментарий', 'comment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const NptTable = memo(({ operations }) => {
|
||||||
|
const [filterValue, setFilterValue] = useState(0)
|
||||||
|
const [npt, setNPT] = useState([])
|
||||||
|
const [filteredNPT, setFilteredNPT] = useState([])
|
||||||
|
const [isTableLoading, setIsTableLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []),
|
||||||
|
setIsTableLoading,
|
||||||
|
'Не удалось получить список НПВ'
|
||||||
|
), [operations])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)),
|
||||||
|
setIsTableLoading,
|
||||||
|
'Не удалось отфильтровать НПВ по времени'
|
||||||
|
), [npt, filterValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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={columns}
|
||||||
|
pagination={false}
|
||||||
|
tableName={'tvd_npt'}
|
||||||
|
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
|
||||||
|
/>
|
||||||
|
</LoaderPortal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default NptTable
|
226
src/pages/WellOperations/Tvd/index.jsx
Normal file
226
src/pages/WellOperations/Tvd/index.jsx
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { memo, useState, useRef, useEffect, useCallback } from 'react'
|
||||||
|
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'
|
||||||
|
import { Switch, Button } from 'antd'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
TimeScale,
|
||||||
|
LinearScale,
|
||||||
|
Legend,
|
||||||
|
LineController,
|
||||||
|
PointElement,
|
||||||
|
LineElement
|
||||||
|
} from 'chart.js'
|
||||||
|
import 'chartjs-adapter-moment'
|
||||||
|
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||||
|
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { formatDate, fractionalSum } from '@utils/datetime'
|
||||||
|
import { getOperations } from '@utils/functions'
|
||||||
|
|
||||||
|
import NptTable from './NptTable'
|
||||||
|
import NetGraphExport from './NetGraphExport'
|
||||||
|
import AdditionalTables from './AdditionalTables'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
import '@styles/tvd.less'
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
TimeScale,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Legend,
|
||||||
|
ChartDataLabels,
|
||||||
|
zoomPlugin
|
||||||
|
)
|
||||||
|
|
||||||
|
const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-'
|
||||||
|
|
||||||
|
const scaleTypes = {
|
||||||
|
day: {
|
||||||
|
min: 0,
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
title: { display: false, text: '' },
|
||||||
|
ticks: { stepSize: 1 }
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
display: true,
|
||||||
|
title: { display: true },
|
||||||
|
type: 'time',
|
||||||
|
time: { unit: 'day', displayFormats: { day: 'DD.MM.YYYY' } },
|
||||||
|
grid: { drawTicks: true },
|
||||||
|
ticks: {
|
||||||
|
stepSize: 3,
|
||||||
|
major: { enabled: true },
|
||||||
|
z: 1,
|
||||||
|
display: true,
|
||||||
|
textStrokeColor: '#fff',
|
||||||
|
textStrokeWidth: 2,
|
||||||
|
color: '#000',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
aspectRatio: false,
|
||||||
|
interaction: { intersect: false, mode: 'point' },
|
||||||
|
scales: {
|
||||||
|
x: scaleTypes.day,
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'top',
|
||||||
|
reverse: true,
|
||||||
|
display: true,
|
||||||
|
title: { display: false, text: '' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parsing: { xAxisKey: 'day', yAxisKey: 'depth' },
|
||||||
|
elements: { point: { radius: 1.7 } },
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
datalabels: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
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 makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
|
||||||
|
label,
|
||||||
|
data,
|
||||||
|
backgroundColor: color,
|
||||||
|
borderColor: color,
|
||||||
|
borderWidth,
|
||||||
|
borderDash,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Tvd = memo(({ idWell, title }) => {
|
||||||
|
const [chart, setChart] = useState()
|
||||||
|
const [xLabel, setXLabel] = useState('day')
|
||||||
|
const [operations, setOperations] = useState({})
|
||||||
|
const [tableVisible, setTableVisible] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const chartRef = useRef(null)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
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).join(',')
|
||||||
|
history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
|
||||||
|
}, [idWell, history])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => setOperations(await getOperations(idWell)),
|
||||||
|
setIsLoading,
|
||||||
|
`Не удалось загрузить операции по скважине "${idWell}"`,
|
||||||
|
'Получение списка опервций по скважине'
|
||||||
|
), [idWell])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const withoutNpt = []
|
||||||
|
operations?.fact?.forEach((row) => {
|
||||||
|
if (row?.isNPT !== false) return
|
||||||
|
const nptH = +(row.nptHours ?? 0)
|
||||||
|
withoutNpt.push({
|
||||||
|
...row,
|
||||||
|
day: row.day - nptH / 24,
|
||||||
|
date: fractionalSum(row.date, -nptH, 'hour'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = { datasets: [
|
||||||
|
makeDataset(operations?.fact, 'Факт', '#0A0', 3),
|
||||||
|
makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]),
|
||||||
|
makeDataset(operations?.plan, 'План', '#F00', 3),
|
||||||
|
makeDataset(withoutNpt, 'Факт без НПВ', '#00F', 3)
|
||||||
|
]}
|
||||||
|
|
||||||
|
if (chartRef.current && !chart) {
|
||||||
|
const thisOptions = {}
|
||||||
|
Object.assign(thisOptions, defaultOptions)
|
||||||
|
thisOptions.onClick = onPointClick
|
||||||
|
thisOptions.scales.x = scaleTypes[xLabel]
|
||||||
|
thisOptions.parsing.xAxisKey = xLabel
|
||||||
|
|
||||||
|
const newChart = new Chart(chartRef.current, {
|
||||||
|
type: 'line',
|
||||||
|
plugins: [ChartDataLabels],
|
||||||
|
options: thisOptions,
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
setChart(newChart)
|
||||||
|
|
||||||
|
return () => chart?.destroy()
|
||||||
|
} else {
|
||||||
|
chart.data = data
|
||||||
|
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, onPointClick])
|
||||||
|
|
||||||
|
const toogleTable = useCallback(() => {
|
||||||
|
setOperations(pre => ({ ...pre }))
|
||||||
|
setTableVisible(v => !v)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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' }}
|
||||||
|
/>
|
||||||
|
<NetGraphExport idWell={idWell} />
|
||||||
|
<Button icon={tableVisible ? <DoubleRightOutlined /> : <DoubleLeftOutlined />} onClick={toogleTable}>НПВ</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<LoaderPortal show={isLoading} style={{ flex: 1 }}>
|
||||||
|
<div className={'tvd-main'}>
|
||||||
|
<div className={'tvd-left'}>
|
||||||
|
<AdditionalTables operations={operations} xLabel={xLabel} setIsLoading={setIsLoading} />
|
||||||
|
<canvas ref={chartRef} />
|
||||||
|
</div>
|
||||||
|
{tableVisible && <NptTable operations={operations?.fact} />}
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Tvd
|
Loading…
Reference in New Issue
Block a user