diff --git a/src/pages/WellOperations/Tvd.jsx b/src/pages/WellOperations/Tvd.jsx deleted file mode 100644 index ae54cb8..0000000 --- a/src/pages/WellOperations/Tvd.jsx +++ /dev/null @@ -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 ( -
-
-

{title || 'График Глубина-день'}

-
- setXLabel(checked ? 'date' : 'day')} - style={{ marginRight: '20px' }} - /> - -
-
- -
-
-
console.log(e)}> - - {printDate(additionalData.endDate)} - {numericRender(additionalData.lag)} - -
- -
- - {numericRender(additionalData.lag)} - {printDate(additionalData.planStartDate)} - {printDate(additionalData.factStartDate)} - {printDate(additionalData.planEndDate)} - {printDate(additionalData.factEndDate)} - -
-
- {tableVisible && ( -
-
- - Фильтр время НПВ ≥ - setFilterValue(value ?? 0)} - value={filterValue} - /> - ч. -
- - - - - )} - - - - ) -}) - -export default Tvd diff --git a/src/pages/WellOperations/Tvd/AdditionalTables.jsx b/src/pages/WellOperations/Tvd/AdditionalTables.jsx new file mode 100644 index 0000000..1889b63 --- /dev/null +++ b/src/pages/WellOperations/Tvd/AdditionalTables.jsx @@ -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 ( + <> +
+ + {printDate(additionalData.endDate)} + {numericRender(additionalData.lag)} + +
+
+ + {numericRender(additionalData.lag)} + {printDate(additionalData.planStartDate)} + {printDate(additionalData.factStartDate)} + {printDate(additionalData.planEndDate)} + {printDate(additionalData.factEndDate)} + +
+ + ) +}) + +export default AdditionalTables diff --git a/src/pages/WellOperations/Tvd/NetGraphExport.jsx b/src/pages/WellOperations/Tvd/NetGraphExport.jsx new file mode 100644 index 0000000..72a5d72 --- /dev/null +++ b/src/pages/WellOperations/Tvd/NetGraphExport.jsx @@ -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 ( + + ) +}) + +export default NetGraphExport diff --git a/src/pages/WellOperations/Tvd/NptTable.jsx b/src/pages/WellOperations/Tvd/NptTable.jsx new file mode 100644 index 0000000..e6187e0 --- /dev/null +++ b/src/pages/WellOperations/Tvd/NptTable.jsx @@ -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 ( +
+
+ + Фильтр время НПВ ≥ + setFilterValue(value ?? 0)} + value={filterValue} + /> + ч. +
+ +
+ + + ) +}) + +export default NptTable diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx new file mode 100644 index 0000000..e0e2784 --- /dev/null +++ b/src/pages/WellOperations/Tvd/index.jsx @@ -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 ( +
+
+

{title || 'График Глубина-день'}

+
+ setXLabel(checked ? 'date' : 'day')} + style={{ marginRight: '20px' }} + /> + + +
+
+ +
+
+ + +
+ {tableVisible && } +
+
+
+ ) +}) + +export default Tvd