Tvd перенесена в отдельную директорию и разделена на файлы

This commit is contained in:
Александр Сироткин 2022-03-18 14:31:50 +05:00
parent 98f20a4ef8
commit 1f5cc759be
5 changed files with 388 additions and 355 deletions

View File

@ -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>Фильтр время НПВ &ge;</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

View 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

View 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

View 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>Фильтр время НПВ &ge;</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

View 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