diff --git a/.vscode/settings.json b/.vscode/settings.json index a8ab85f..01228b7 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,17 @@ { "cSpell.words": [ - "день" + "день", + "спиннера", + "Saub", + "КНБК", + "САУБ", + "antd", + "Poprompt", + "saub", + "setpoint", + "Setpoints", + "usehooks" ], - "liveServer.settings.port": 5501 + "liveServer.settings.port": 5501, + "cSpell.language": "en,ru" } \ No newline at end of file diff --git a/CODE_STANDART.md b/CODE_STANDART.md new file mode 100644 index 0000000..92dce3a --- /dev/null +++ b/CODE_STANDART.md @@ -0,0 +1,170 @@ +## 1. Общие положения +1. Все несамостоятельные компоненты должны быть написаны на TypeScript. Для самостоятельных компонентов (использующихся как страницы) (далее страницы) допускается использование JavaScript для ускорения написания; + +### 1.1. Файловая структура проекта +1. Компоненты должны распределяться по директориям в соответствии со своим назначением: + * `src/context` - Для контекстов приложения; + * `src/components` - Для несамостоятельных компонентов, применяющихся многократно; + * `src/pages` - Для страниц и компонентов, использующихся исключительно в единственном экземпляре; + * `src/images` - Для компонентов-изображений. +2. Если страница описывается 1 файлом она должна именоваться в соответствии с содержимым, в ином случае должна быть создана директория с соответствующим названием, внутри которой будут находиться файлы страницы. Основной файл в таком случае должен быть переименован в `index.jsx`; +3. Файлы именуются в соответствии с таблицей: + | Тип содержимого файла | Расширение | Стиль именования | + |--------------------------------------|------------|--------------------------| + | Компонент или страница | jsx/tsx | **PascalCase** | + | Файл стилей | css/less | **snake_case** | + | Вспомогательные методы или константы | js/ts | **snake_case** | + | Описательные документы | md | **SCREAMING_SNAKE_CASE** | + +### 1.2. Стилизация кода +1. Все строки должны по возможности описываться одинарными кавычками или при необходимости обратными: + ```js + const name = 'world' + const msg = 'Hello, \'' + name + '\'!' + const toPrint = `Message: ${msg}` + ``` +2. Все переменные по возможности должны инициализироваться как `const`, применение `var` не допускается; +3. Переменные именуются в соответствии с таблицей: + | Тип переменной | Стиль именования | + |-------------------|--------------------------| + | Метод, переменная | **camelCase** | + | Константы | **SCREAMING_SNAKE_CASE** | + | Компонент | **PascalCase** | + +### 1.3. Импортирование / Экспортирование +1. Импортированные файлы (в том числе lazy import) необходимо указывать в самом верху документа в следующем порядке с разделением пустой строкой: + 1. Внешние зависимости (`react`, `antd`, `webpack` и т.д.); + 2. Локальные компоненты по порядку: + 1. Контексты (`@asb/context`); + 2. Компоненты (`@components/Table`); + 3. Вспомогательные методы (`@utils`); + 4. Сервисы API (`@api`). + 3. Изображения и компоненты-изображения (`@images`); + 4. Стили (`@styles`); + 5. Lazy import (`const page = React.lazy(() => import('./page'))`). +2. При импорте локальных файлов стоит пользоваться alias'ами: + | Путь | Alias | + |------------------|--------------| + | src/components | @components | + | src/context | @asb/context | + | src/images | @images | + | src/pages | @pages | + | src/services/api | @api | + | src/styles | @styles | + | src/utils | @utils | +3. По возможности импортировать из пакетов и файлов только использующиеся сущности: + ```tsx + // вместо + import React from 'react' + const page: React.ReactNode = React.lazy(() => import (...)) + + // стоит использовать + import { lazy, ReactNode } from 'react' + const page: ReactNode = lazy(() => import (...)) + ``` + +## 2. JS +1. Методы, константы и переменные документируются в соответствии с `JSDoc`; +2. При документации страниц необходимо указать её название, краткое описание и описание получаемых параметров: + ```jsx + import { memo } from 'react' + + import LoaderPortal from '@components/LoaderPortal' + + /** + * Тестовая страница + * + * @description Данная страница не имеет смысла и просто выводит переданное название и контент + * @param title - Название страницы + * @param content - Контент страницы + * @param loading - Отображать ли оверлей загрузки над блоком страницы + */ + export const TestPage = memo(({ title, content, loading }) => ( + +
+
{title}
+
{content}
+
+
+ )) + + export default TestPage + ``` + +## 3. TS +1. Методы, константы и переменные документируются в соответствии с `TSDoc`; +2. При документации компонентов необходимо указать их название, краткое описание, а также описать параметры в типе: + ```tsx + import { memo } from 'react' + + import LoaderPortal from '@components/LoaderPortal' + + export type TestPageProps = { + /** Название страницы */ + title: ReactNode + /** Контент страницы */ + content: ReactNode + /** Отображать ли оверлей загрузки над блоком страницы */ + loading: boolean + } + + /** + * Тестовая страница + * + * @description Данная страница не имеет смысла и просто выводит переданное название и контент + */ + export const TestPage = memo(({ title, content, loading }) => ( + +
+
{title}
+
{content}
+
+
+ )) + + export default TestPage + ``` +3. Использование `any` в типах допустимо только, если значение используется только в параметрах компонентов, обозначенных типом `any`. Если метод предполагает работу с разными типами значений стоит описать его как обобщённый. + + +## 4. JSX/TSX + +### 4.1. Стилизация кода +1. Все указываемые к компоненту параметры должны быть обёрнуты в фигурные скобки, кроме параметров флагов со значением `true`: + ```jsx + + ``` +2. Если описание параметров компонента не укладывается в ширину в 120 строк стоит перенести их в соответствии с шаблоном: + ```jsx + + ``` +3. Если JSX код передаётся как значение стоит обернуть его в круглые скобки: + ```jsx + const a = ( + + ) + ``` + +### 4.2. Логика поведения +1. Не допускается создание значений ссылочных типов в области рендера. Они должны быть вынесены в переменные или константы; +2. Не допускается создание переменных в функциональных компонентов без использования хуков `useMemo`/`useCallback`/`useState`; +3. Если переменные или методы не имеют зависимостей и не вызывают методы, доступные исключительно внутри компонента, они должны быть вынесены выше кода компонента. + + +## 5. LESS +1. Использование id должно быть сведено к минимуму; +2. Все классы именуются с префиксом компании "`dd-`"; +3. Слова в классах разделяются тире ("`-`"); +4. Файлы именуются в соответствии с компонентом, к которому относятся; +5. В одном файле описываются стили либо к конкретному компоненту, либо к странице; +6. Файл со стилями должен подключаться не более чем к одному компоненту (странице); +7. Файлы поделены на директории виду компонента, к которому применяются стили: + * `styles/components` - для компонентов, не использующихся самостоятельно; + * `styles/pages` - для компонентов, использующихся как страница; + * `styles/widgets` - для компонентов, применяющихся как виджеты в дашбордах. diff --git a/README.md b/README.md index cc48f9f..8772bdf 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Установка выполняется одной командой: ```bash -npm i +npm ci ``` ## 2. Автогенерация сервисов @@ -17,14 +17,14 @@ npm i Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную. -Если сервер запущен на текущей машине достаточно написать: +Если сервер запущен на текущей машине достаточно написать: ```bash -npm run update_openapi +npm run oul ``` -Для получения сервисов с основного сервера: +Для получения сервисов с основного сервера: ```bash -npm run update_openapi_server +npm run oug_dev ``` или же ручной вариант: @@ -36,12 +36,12 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service На данный момент имеются следующие IP-адреса: -| IP-адрес | Описание | -|:-|:-| -| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) | -| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) | -| 46.146.209.148:89 | Внешний адрес development-сервера | -| cloud.digitaldrilling.ru | Внешний адрес production-сервера | +| IP-адрес | Команда | Описание | +|:-------------------------|:--------|:------------------------------------| +| 127.0.0.1:5000 | oul | Локальный адрес вашей машины | +| 192.168.1.113:5000 | oud | Локальный адрес development-сервера | +| 46.146.209.148:89 | oug_dev | Внешний адрес development-сервера | +| cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера | ## 3. Компиляция production-версии приложения После выполнения вышеописанных пунктов приложение готово к компиляции. diff --git a/package-lock.json b/package-lock.json index 3cf958f..8912fac 100755 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "react-dom": "^18.1.0", "react-router-dom": "^6.3.0", "rxjs": "^7.5.5", - "usehooks-ts": "^2.6.0", - "web-vitals": "^2.1.4" + "usehooks-ts": "^2.6.0" }, "devDependencies": { "@babel/core": "^7.18.2", @@ -14786,11 +14785,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -26492,11 +26486,6 @@ "minimalistic-assert": "^1.0.0" } }, - "web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/src/App.tsx b/src/App.tsx index 0ac3eb0..8f11027 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,15 @@ import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom' import { lazy, memo, Suspense } from 'react' -import locale from 'antd/lib/locale/ru_RU' -import { ConfigProvider } from 'antd' import { RootPathContext } from '@asb/context' -import { UserOutlet } from '@components/outlets' -import LayoutPortal from '@components/LayoutPortal' import SuspenseFallback from '@components/SuspenseFallback' -import { getUser, NoAccessComponent } from '@utils' -import { OpenAPI } from '@api' +import { NoAccessComponent } from '@utils' -import '@styles/include/antd_theme.less' -import '@styles/App.less' +import '@styles/pages/App.less' + +const UserOutlet = lazy(() => import('@components/outlets/UserOutlet')) +const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet')) +const LayoutPortal = lazy(() => import('@components/LayoutPortal')) const Login = lazy(() => import('@pages/public/Login')) const Register = lazy(() => import('@pages/public/Register')) @@ -22,28 +20,23 @@ const Deposit = lazy(() => import('@pages/Deposit')) const Cluster = lazy(() => import('@pages/Cluster')) const Well = lazy(() => import('@pages/Well')) -// OpenAPI.BASE = 'http://localhost:3000' -// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости -OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || '' -OpenAPI.HEADERS = { 'Content-Type': 'application/json' } - export const App = memo(() => ( - - - }> - - - } /> - } /> + + }> + + + } /> + } /> - {/* Public pages */} - } /> - } /> + {/* Public pages */} + } /> + } /> - {/* User pages */} - }> - } /> + {/* User pages */} + }> + } /> + }> }> {/* Admin pages */} } /> @@ -54,11 +47,11 @@ export const App = memo(() => ( } /> - - - - - + + + + + )) export default App diff --git a/src/components/CopyUrl.tsx b/src/components/CopyUrl.tsx index 22e7747..2a157f4 100644 --- a/src/components/CopyUrl.tsx +++ b/src/components/CopyUrl.tsx @@ -1,5 +1,5 @@ import { cloneElement, memo, useCallback, useMemo, useState } from 'react' -import { Button, ButtonProps } from 'antd' +import { Button, ButtonProps, Tooltip } from 'antd' import { CopyOutlined } from '@ant-design/icons' import { invokeWebApiWrapperAsync, notify } from './factory' @@ -43,11 +43,9 @@ export type CopyUrlButtonProps = Omit & ButtonProps export const CopyUrlButton = memo(({ sendLoading, hideUnsupported, onCopy, ...other }) => { return ( - ), - render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => ( + render: (visible, _, index = NaN) => ( , 'onChange'> & { + /** Текущее значение */ value?: TimeDto, + /** Метод вызывается при изменений времени */ onChange?: (date: TimeDto | null) => any + /** Конвертировать ли время в UTC */ isUTC?: boolean } diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index fd5d017..bb9a292 100755 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -1,4 +1,3 @@ -export * from './sorters' export * from './EditableTable' export * from './DatePickerWrapper' export * from './TimePickerWrapper' @@ -18,6 +17,13 @@ export type PaginationContainer = { items?: T[] | null } +/** + * Генерирует объект пагинации для компонента `Table` из данных от сервисов + * + * @param сontainer данные от сервиса + * @param other Дополнительные поля (передаются в объект напрямую в приоритете) + * @returns Объект пагинации + */ export const makePaginationObject = (сontainer: PaginationContainer, other: M) => ({ ...other, pageSize: сontainer.take, diff --git a/src/components/Table/sorters.ts b/src/components/Table/sorters.ts deleted file mode 100755 index 6f50401..0000000 --- a/src/components/Table/sorters.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { timeToMoment } from '@utils' -import { isRawDate } from '@utils' -import { TimeDto } from '@api' - -import { DataType } from './Columns' -import { CompareFn } from 'antd/lib/table/interface' - -export const makeNumericSorter = (key: keyof DataType): CompareFn> => - (a: DataType, b: DataType) => Number(a[key]) - Number(b[key]) - -export const makeNumericObjSorter = (key: [string, string]) => - (a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]]) - -export const makeStringSorter = (key: keyof DataType) => (a?: DataType | null, b?: DataType | null) => { - if (!a && !b) return 0 - if (!a) return 1 - if (!b) return -1 - - return String(a[key]).localeCompare(String(b[key])) -} - -export const makeDateSorter = (key: keyof DataType) => (a: DataType, b: DataType) => { - const adate = a[key] - const bdate = b[key] - if (!isRawDate(adate) || !isRawDate(bdate)) - throw new Error('Date column contains not date formatted string(s)') - - const date = new Date(adate) - - return date.getTime() - new Date(bdate).getTime() -} - -export const makeTimeSorter = (key: keyof DataType) => (a: DataType, b: DataType) => { - const elma = a[key] - const elmb = b[key] - - if (!elma && !elmb) return 0 - if (!elma) return 1 - if (!elmb) return -1 - - return timeToMoment(elma).diff(timeToMoment(elmb)) -} diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index f222420..f9a380b 100755 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import { isURLAvailable, removeUser } from '@utils' import { AuthService } from '@api' -import '@styles/user_menu.less' +import '@styles/components/user_menu.less' export type UserMenuProps = DrawerProps & { isAdmin?: boolean diff --git a/src/components/d3/D3Chart.tsx b/src/components/d3/D3Chart.tsx index 2aedeab..9e8bee9 100644 --- a/src/components/d3/D3Chart.tsx +++ b/src/components/d3/D3Chart.tsx @@ -36,7 +36,7 @@ import type { ChartTicks } from './types' -import '@styles/d3.less' +import '@styles/components/d3.less' const defaultOffsets: ChartOffset = { top: 10, diff --git a/src/components/d3/D3HorizontalPercentChart.tsx b/src/components/d3/D3HorizontalPercentChart.tsx index 0b04bd0..17ea77b 100644 --- a/src/components/d3/D3HorizontalPercentChart.tsx +++ b/src/components/d3/D3HorizontalPercentChart.tsx @@ -4,10 +4,10 @@ import { Property } from 'csstype' import * as d3 from 'd3' import LoaderPortal from '@components/LoaderPortal' +import { usePartialProps } from '@utils' import { ChartOffset } from './types' -import '@styles/d3.less' -import { usePartialProps } from '@asb/utils' +import '@styles/components/d3.less' export type PercentChartDataType = { name: string diff --git a/src/components/d3/D3MouseZone.tsx b/src/components/d3/D3MouseZone.tsx index 464d814..72b1730 100644 --- a/src/components/d3/D3MouseZone.tsx +++ b/src/components/d3/D3MouseZone.tsx @@ -3,7 +3,7 @@ import * as d3 from 'd3' import { ChartOffset } from './types' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3MouseState = { /** Позиция мыши по оси X */ diff --git a/src/components/d3/monitoring/D3HorizontalCursor.tsx b/src/components/d3/monitoring/D3HorizontalCursor.tsx index c2ee50d..d1caa2e 100644 --- a/src/components/d3/monitoring/D3HorizontalCursor.tsx +++ b/src/components/d3/monitoring/D3HorizontalCursor.tsx @@ -9,9 +9,9 @@ import { getChartIcon, isDev, usePartialProps } from '@utils' import { BaseDataType } from '../types' import { ChartGroup, ChartSizes } from './D3MonitoringCharts' -import '@styles/d3.less' +import '@styles/components/d3.less' -type D3GroupRenderFunction = (group: ChartGroup, data: DataType[]) => ReactNode +type D3GroupRenderFunction = (group: ChartGroup, data: DataType[], flowData: DataType[] | undefined) => ReactNode export type D3HorizontalCursorSettings = { width?: number @@ -27,6 +27,7 @@ export type D3HorizontalCursorSettings = { export type D3HorizontalCursorProps = D3HorizontalCursorSettings & { groups: ChartGroup[] data: DataType[] + flowData: DataType[] | undefined sizes: ChartSizes yAxis?: d3.ScaleTime spaceBetweenGroups?: number @@ -38,7 +39,7 @@ const defaultLineStyle: SVGProps = { const offsetY = 5 -const makeDefaultRender = (): D3GroupRenderFunction => (group, data) => ( +const makeDefaultRender = (): D3GroupRenderFunction => (group, data, flowData) => ( <> {data.length > 0 ? group.charts.map((chart) => { const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}` @@ -74,6 +75,7 @@ const _D3HorizontalCursor = ({ lineStyle: _lineStyle, data, + flowData, groups, sizes, yAxis, @@ -167,7 +169,7 @@ const _D3HorizontalCursor = ({ return (date >= currentDate - limitInS) && (date <= currentDate + limitInS) }) - const bodies = groups.map((group) => render(group, chartData)) + const bodies = groups.map((group) => render(group, chartData, flowData)) setTooltipBodies(bodies) }, [groups, data, yAxis, lineY, fixed, mouseState.visible]) @@ -190,7 +192,7 @@ const _D3HorizontalCursor = ({ >
{tooltipBodies[i]} diff --git a/src/components/d3/monitoring/D3MonitoringCharts.tsx b/src/components/d3/monitoring/D3MonitoringCharts.tsx index dbc2a54..9fc9c3b 100644 --- a/src/components/d3/monitoring/D3MonitoringCharts.tsx +++ b/src/components/d3/monitoring/D3MonitoringCharts.tsx @@ -35,13 +35,14 @@ const roundTo = (v: number, to: number = 50) => { return (v > 0 ? Math.ceil : Math.round)(v / to) * to } +const getNear = (n: number) => { + let k = 0 + for (let c = Math.abs(n); c >= 1; c /= 10) k++ + return Math.pow(10, k) * Math.sign(n) +} + const calculateDomain = (mm: MinMax): Required => { - let round = Math.abs((mm.max ?? 0) - (mm.min ?? 0)) - if (round < 10) round = 10 - else if (round < 100) round = roundTo(round, 10) - else if (round < 1000) round = roundTo(round, 100) - else if (round < 10000) round = roundTo(round, 1000) - else round = 0 + const round = getNear(Math.abs((mm.max ?? 0) - (mm.min ?? 0))) || 10 let min = roundTo(mm.min ?? 0, round) let max = roundTo(mm.max ?? round, round) if (round && Math.abs(min - max) < round) { @@ -73,8 +74,8 @@ export type ChartGroup = { } const defaultOffsets: ChartOffset = { - top: 10, - bottom: 10, + top: 0, + bottom: 0, left: 100, right: 20, } @@ -115,6 +116,8 @@ export type D3MonitoringChartsProps = Omit /** Цвет фона в формате CSS-значения */ @@ -180,6 +183,7 @@ const _D3MonitoringCharts = >({ loading = false, datasetGroups, data, + flowData, plugins, offset: _offset, yAxis: _yAxisConfig, @@ -193,6 +197,7 @@ const _D3MonitoringCharts = >({ methods, className = '', + style, ...other }: D3MonitoringChartsProps) => { const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups) @@ -241,11 +246,11 @@ const _D3MonitoringCharts = >({ if (!data) return const yAxis = d3.scaleTime() - .domain([yDomain?.min ?? 0, yDomain?.max ?? 0]) + .domain([yDomain?.min || 0, yDomain?.max || 0]) .range([0, sizes.chartsHeight]) return yAxis - }, [groups, data, yDomain, sizes.chartsHeight]) + }, [groups, data, yDomain, sizes]) const chartDomains = useMemo(() => groups.map((group) => { const out: [string | number, ChartDomain][] = group.charts.map((chart) => { @@ -351,10 +356,10 @@ const _D3MonitoringCharts = >({ x: getByAccessor(dataset.xAxis?.accessor), } ) - + if (newChart.type === 'line') newChart.optimization = false - + // Если у графика нет группы создаём её if (newChart().empty()) group().append('g') @@ -452,7 +457,7 @@ const _D3MonitoringCharts = >({ .tickSize(yTicks.visible ? -width + offset.left + offset.right : 0) .ticks(yTicks.count) as any // TODO: Исправить тип ) - + yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color) }, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks]) @@ -462,8 +467,8 @@ const _D3MonitoringCharts = >({ groups.forEach((group, i) => { group() .attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`) - .attr('clip-path', `url(#chart-clip)`) - + .attr('clip-path', `url(#chart-group-clip)`) + group.charts.forEach((chart) => { chart() .attr('color', chart.color || null) @@ -491,21 +496,21 @@ const _D3MonitoringCharts = >({ chartData = renderArea(xAxis, yAxis, chart, chartData) break case 'rect_area': - renderRectArea(xAxis, yAxis, chart) + renderRectArea(xAxis, yAxis, chart, flowData) break default: break } - + if (chart.point) renderPoint(xAxis, yAxis, chart, chartData, true) if (dash) chart().attr('stroke-dasharray', dash) - + chart.afterDraw?.(chart) }) }) - }, [data, groups, height, offset, sizes, chartDomains]) + }, [data, flowData, groups, height, offset, sizes, chartDomains, yAxis]) return ( >({ style={{ width: givenWidth, height: givenHeight, + ...style, }} >
>({ > - + {/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */} @@ -569,6 +575,7 @@ const _D3MonitoringCharts = >({ sizes={sizes} spaceBetweenGroups={spaceBetweenGroups} data={data} + flowData={flowData} height={height} /> diff --git a/src/components/d3/monitoring/D3MonitoringLimitChart.tsx b/src/components/d3/monitoring/D3MonitoringLimitChart.tsx index d58bdde..7e2aaec 100644 --- a/src/components/d3/monitoring/D3MonitoringLimitChart.tsx +++ b/src/components/d3/monitoring/D3MonitoringLimitChart.tsx @@ -92,6 +92,15 @@ const _D3MonitoringLimitChart = ({ const [ref, setRef] = useState(null) const [selected, setSelected] = useState() + const selectedRegulator = useMemo(() => { + if (!selected) return null + const out = regulators[selected.id] || { + label: `ID = ${selected.id}`, + color: 'black', + } + return out + }, [selected, regulators]) + const data = useMemo(() => calcualteData(chartData), [chartData]) useEffect(() => { @@ -105,7 +114,7 @@ const _D3MonitoringLimitChart = ({ .attr('width', width) .attr('height', (d) => Math.max(yAxis(d.dateEnd) - yAxis(d.dateStart), 1)) .attr('y', (d) => yAxis(d.dateStart)) - .attr('fill', (d) => regulators[d.id].color) + .attr('fill', (d) => regulators[d.id]?.color || 'black') .on('mouseover', (_, d) => { const y = yAxis(d.dateStart) - tooltipHeight setSelected({ ...d, y, x: -tooltipWidth - 10, visible: true }) @@ -130,14 +139,24 @@ const _D3MonitoringLimitChart = ({ return ( - - + + + {/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */} + + + + + + + + {selected && ( @@ -148,7 +167,7 @@ const _D3MonitoringLimitChart = ({ y={zoneY1} width={zoneWidth} height={zoneY2 - zoneY1} - fill={regulators[selected.id].color} + fill={selectedRegulator?.color} /> )} @@ -158,7 +177,7 @@ const _D3MonitoringLimitChart = ({
Ограничивающий параметр - {regulators[selected.id].label} + {selectedRegulator?.label} Начало: {formatDate(selected.dateStart)} diff --git a/src/components/d3/plugins/D3Cursor.tsx b/src/components/d3/plugins/D3Cursor.tsx index 2b2adab..3769981 100644 --- a/src/components/d3/plugins/D3Cursor.tsx +++ b/src/components/d3/plugins/D3Cursor.tsx @@ -6,7 +6,7 @@ import { usePartialProps } from '@utils' import { wrapPlugin } from './base' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3CursorSettings = { /** Параметры стиля линии */ diff --git a/src/components/d3/plugins/D3Tooltip.tsx b/src/components/d3/plugins/D3Tooltip.tsx index 3517b5f..1d16cb7 100644 --- a/src/components/d3/plugins/D3Tooltip.tsx +++ b/src/components/d3/plugins/D3Tooltip.tsx @@ -8,7 +8,7 @@ import { BaseDataType, ChartRegistry } from '@components/d3/types' import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone' import { getTouchedElements, wrapPlugin } from './base' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none' diff --git a/src/components/d3/renders/rect_area.ts b/src/components/d3/renders/rect_area.ts index d1d074c..7cc29a2 100644 --- a/src/components/d3/renders/rect_area.ts +++ b/src/components/d3/renders/rect_area.ts @@ -6,7 +6,8 @@ import { appendTransition } from './base' export const renderRectArea = ( xAxis: (value: d3.NumberValue) => number, yAxis: (value: d3.NumberValue) => number, - chart: ChartRegistry + chart: ChartRegistry, + data: DataType[] | undefined, ) => { if ( chart.type !== 'rect_area' || @@ -14,25 +15,27 @@ export const renderRectArea = ( !chart.maxXAccessor || !chart.minYAccessor || !chart.maxYAccessor || - !chart.data + !data ) return - const data = chart.data const xMin = getByAccessor(chart.minXAccessor) const xMax = getByAccessor(chart.maxXAccessor) const yMin = getByAccessor(chart.minYAccessor) const yMax = getByAccessor(chart.maxYAccessor) - chart().attr('fill', 'currentColor') + chart() + .attr('fill', 'currentColor') + .attr('fill-opacity', '0.15') + .attr('stroke-opacity', '0.3') const rects = chart().selectAll('rect').data(data) rects.exit().remove() rects.enter().append('rect') - + appendTransition(chart().selectAll>('rect'), chart) - .attr('x1', (d) => xAxis(xMin(d))) - .attr('x2', (d) => xAxis(xMax(d))) - .attr('y1', (d) => yAxis(yMin(d))) - .attr('y2', (d) => yAxis(yMax(d))) + .attr('x', (d) => xAxis(xMin(d))) + .attr('y', (d) => yAxis(yMin(d))) + .attr('width', (d) => xAxis(xMax(d)) - xAxis(xMin(d))) + .attr('height', (d) => yAxis(yMax(d)) - yAxis(yMin(d))) } diff --git a/src/components/outlets/DepositsOutlet.tsx b/src/components/outlets/DepositsOutlet.tsx new file mode 100644 index 0000000..80e2303 --- /dev/null +++ b/src/components/outlets/DepositsOutlet.tsx @@ -0,0 +1,35 @@ +import { memo, useEffect, useState } from 'react' +import { Outlet } from 'react-router-dom' + +import { DepositListContext } from '@asb/context' +import LoaderPortal from '@components/LoaderPortal' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { DepositDto, DepositService } from '@api' +import { arrayOrDefault } from '@utils' + +export const DepositsOutlet = memo(() => { + const [deposits, setDeposits] = useState([]) + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + const deposits = await DepositService.getDeposits() + setDeposits(arrayOrDefault(deposits)) + }, + setIsLoading, + `Не удалось загрузить список кустов`, + { actionName: 'Получить список кустов' } + ) + }, []) + + return ( + + + + + + ) +}) + +export default DepositsOutlet diff --git a/src/components/outlets/index.ts b/src/components/outlets/index.ts index 77c9a70..3542fbb 100644 --- a/src/components/outlets/index.ts +++ b/src/components/outlets/index.ts @@ -1 +1,2 @@ +export * from './DepositsOutlet' export * from './UserOutlet' diff --git a/src/components/selectors/WellSelector.jsx b/src/components/selectors/WellSelector.jsx index ba448be..1894021 100755 --- a/src/components/selectors/WellSelector.jsx +++ b/src/components/selectors/WellSelector.jsx @@ -1,12 +1,11 @@ import { Tag, TreeSelect } from 'antd' import { memo, useEffect, useState } from 'react' +import { useDepositList } from '@asb/context' import { invokeWebApiWrapperAsync } from '@components/factory' import { hasPermission } from '@utils' -import { DepositService } from '@api' -export const getTreeData = async () => { - const deposits = await DepositService.getDeposits() +export const getTreeData = async (deposits) => { const wellsTree = deposits.map((deposit, dIdx) => ({ title: deposit.caption, key: `0-${dIdx}`, @@ -40,10 +39,12 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot const [wellsTree, setWellsTree] = useState([]) const [wellLabels, setWellLabels] = useState([]) + const deposits = useDepositList() + useEffect(() => { invokeWebApiWrapperAsync( async () => { - const wellsTree = treeData ?? await getTreeData() + const wellsTree = treeData ?? await getTreeData(deposits) const labels = treeLabels ?? getTreeLabels(wellsTree) setWellsTree(wellsTree) setWellLabels(labels) @@ -52,12 +53,13 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot 'Не удалось загрузить список скважин', { actionName: 'Получение списка скважин' } ) - }, [treeData, treeLabels]) + }, [deposits, treeData, treeLabels]) return ( idState === 1 ? 'active' : 'unknown' export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean => isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000) -const getKeyByUrl = (url?: string): [Key | null, string | null] => { - const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id" - if (!result) return [null, null] - return [result[0], result[1]] +const getKeyByUrl = (url?: string): [Key | null, string | null, number | null] => { + const result = url?.match(URL_REGEX) // pattern "/:type/:id" + if (!result) return [null, null, null] + return [result[0], result[1], result[2] && result[2] !== 'null' ? Number(result[2]) : null] } const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => { - const [url, type] = getKeyByUrl(value) + const [url, type, key] = getKeyByUrl(value) if (!url) return let deposit: TreeDataNode | undefined let cluster: TreeDataNode | undefined let well: TreeDataNode | undefined switch (type) { case 'deposit': + if (key === null) return 'Месторождение не выбрано' deposit = wellsTree.find((deposit) => deposit.key === url) if (deposit) return `${deposit.title}` return 'Ошибка! Месторождение не найдено!' case 'cluster': + if (key === null) return 'Куст не выбран' deposit = wellsTree.find((deposit) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url) )) @@ -44,6 +53,7 @@ const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined return 'Ошибка! Куст не найден!' case 'well': + if (key === null) return 'Скважина не выбрана' deposit = wellsTree.find((deposit) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => ( well = cluster.children?.find((well: TreeDataNode) => well.key === url) @@ -91,62 +101,44 @@ const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): return out } +const makeWellsTreeData = (deposits: DepositDto[]): TreeDataNode[] => deposits.map(deposit =>({ + title: deposit.caption, + key: `/deposit/${deposit.id}`, + value: `/deposit/${deposit.id}`, + icon: , + children: deposit.clusters?.map(cluster => { + const wells = cluster.wells ? cluster.wells.slice() : [] + wells.sort(sortWellsByActive) + + return { + title: cluster.caption, + key: `/cluster/${cluster.id}`, + value: `/cluster/${cluster.id}`, + icon: , + children: wells.map(well => ({ + title: well.caption, + key: `/well/${well.id}`, + value: `/well/${well.id}`, + icon: + })), + } + }), +})) + export const WellTreeSelector = memo(({ expand, current, onChange, onClose, open, ...other }) => { - const [wellsTree, setWellsTree] = useState([]) - const [showLoader, setShowLoader] = useState(false) const [expanded, setExpanded] = useState([]) const [selected, setSelected] = useState([]) const navigate = useNavigate() const location = useLocation() + const deposits = useDepositList() - useEffect(() => { - if (current) setSelected([current]) - }, [current]) - - useEffect(() => { - setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev) - }, [wellsTree, expand]) - - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const deposits: Array = await DepositService.getDeposits() - const wellsTree: TreeDataNode[] = deposits.map(deposit =>({ - title: deposit.caption, - key: `/deposit/${deposit.id}`, - value: `/deposit/${deposit.id}`, - icon: , - children: deposit.clusters?.map(cluster => { - const wells = cluster.wells ? cluster.wells.slice() : [] - wells.sort(sortWellsByActive) - - return { - title: cluster.caption, - key: `/cluster/${cluster.id}`, - value: `/cluster/${cluster.id}`, - icon: , - children: wells.map(well => ({ - title: well.caption, - key: `/well/${well.id}`, - value: `/well/${well.id}`, - icon: - })), - } - }), - })) - setWellsTree(wellsTree) - }, - setShowLoader, - `Не удалось загрузить список скважин`, - { actionName: 'Получить список скважин' } - ) - }, []) + const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits]) const onValueChange = useCallback((value?: string): void => { const key = getKeyByUrl(value)[0] @@ -155,8 +147,8 @@ export const WellTreeSelector = memo(({ expand, current, }, [wellsTree]) const onSelect = useCallback((value: Key[]): void => { - const newRoot = /\/(\w+)\/\d+/.exec(String(value)) - const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname) + const newRoot = URL_REGEX.exec(String(value)) + const oldRoot = URL_REGEX.exec(location.pathname) if (!newRoot || !oldRoot) return let newPath = newRoot[0] @@ -169,21 +161,27 @@ export const WellTreeSelector = memo(({ expand, current, navigate(newPath, { state: { from: location.pathname }}) }, [navigate, location]) - useEffect(() => onValueChange(location.pathname), [onValueChange, location]) + useEffect(() => { + if (current) setSelected([current]) + }, [current]) + + useEffect(() => { + setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev) + }, [wellsTree, expand]) + + useEffect(() => onValueChange(location.pathname), [onValueChange, location.pathname]) return ( - - - + ) }) diff --git a/src/components/views/CompanyView.tsx b/src/components/views/CompanyView.tsx index db012a5..647100a 100755 --- a/src/components/views/CompanyView.tsx +++ b/src/components/views/CompanyView.tsx @@ -9,6 +9,7 @@ export type CompanyViewProps = { company?: CompanyDto } +/** Компонент для отображения информации о компании */ export const CompanyView = memo(({ company }) => company ? ( diff --git a/src/components/views/PermissionView.tsx b/src/components/views/PermissionView.tsx index 7708be5..9c185a7 100755 --- a/src/components/views/PermissionView.tsx +++ b/src/components/views/PermissionView.tsx @@ -8,6 +8,7 @@ export type PermissionViewProps = { info?: PermissionDto } +/** Компонент для отображения информации о разрешении */ export const PermissionView = memo(({ info }) => info ? ( diff --git a/src/components/views/RoleView.tsx b/src/components/views/RoleView.tsx index 8974419..66c0ad3 100755 --- a/src/components/views/RoleView.tsx +++ b/src/components/views/RoleView.tsx @@ -9,6 +9,7 @@ export type RoleViewProps = { role?: UserRoleDto } +/** Компонент для отображения информации о роли */ export const RoleView = memo(({ role }) => { if (!role) return ( - ) diff --git a/src/components/views/TelemetryView.tsx b/src/components/views/TelemetryView.tsx index 8758353..cec6093 100755 --- a/src/components/views/TelemetryView.tsx +++ b/src/components/views/TelemetryView.tsx @@ -19,6 +19,12 @@ export const lables: Record = { spinPlcVersion: 'Версия Спин Мастер', } +/** + * Строит название для телеметрии + * + * @param telemetry Объект телеметрии + * @returns Название + */ export const getTelemetryLabel = (telemetry?: TelemetryDto) => `${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}` @@ -26,6 +32,7 @@ export type TelemetryViewProps = { telemetry?: TelemetryDto } +/** Компонент для отображения информации о телеметрии */ export const TelemetryView = memo(({ telemetry }) => telemetry?.info ? ( & { user?: UserDto } +/** Компонент для отображения информации о пользователе */ export const UserView = memo(({ user, ...other }) => user ? ( = { export type WellViewProps = TooltipProps & { well?: WellDto + iconProps?: DetailedHTMLProps, HTMLSpanElement> + labelProps?: DetailedHTMLProps, HTMLSpanElement> } -export const WellView = memo(({ well, ...other }) => well ? ( +/** + * Получить название скважины + * @param well Объект с данными скважины + * @returns Название скважины + */ +export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}` + +/** Компонент для отображения информации о скважине */ +export const WellView = memo(({ well, iconProps, labelProps, ...other }) => well ? ( Название: @@ -47,10 +57,12 @@ export const WellView = memo(({ well, ...other }) => well ? ( {well.id ?? '---'} )}> - + - {well.caption} + + {getWellTitle(well)} + ) : ( - diff --git a/src/components/views/WirelineView.tsx b/src/components/views/WirelineView.tsx index 5ea9bd0..be0b3f9 100644 --- a/src/components/views/WirelineView.tsx +++ b/src/components/views/WirelineView.tsx @@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & { buttonProps?: ButtonProps } +/** Компонент для отображения информации о талевом канате */ export const WirelineView = memo(({ wireline, buttonProps, ...other }) => ( void]>([{}, () => {}]) -/** Контекст текущего корневого пути */ -export const RootPathContext = createContext('/') -/** Контекст текущего пользователя */ -export const UserContext = createContext({}) -/** Контекст метода редактирования параметров заголовка и меню */ -export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {}) -/** Контекст для блока справа от крошек на страницах скважин и админки */ -export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {}) - -/** - * Получить текущую скважину - * - * @returns Текущая скважина, либо `null` - */ -export const useWell = () => useContext(WellContext) - -/** - * Получить текущий корневой путь - * - * @returns Текущий корневой путь - */ -export const useRootPath = () => useContext(RootPathContext) - -/** - * Получить текущего пользователя - * - * @returns Текущий пользователь, либо `null` - */ -export const useUser = () => useContext(UserContext) - -export const useTopRightBlock = () => useContext(TopRightBlockContext) - -/** - * Получить метод задания параметров заголовка и меню - * - * @returns Получить метод задания параметров заголовка и меню - */ -export const useLayoutProps = (props?: LayoutPortalProps) => { - const setLayoutProps = useContext(LayoutPropsContext) - - useEffect(() => { - if (props) setLayoutProps(props) - }, [setLayoutProps, props]) - - return setLayoutProps -} diff --git a/src/context/deposit.ts b/src/context/deposit.ts new file mode 100644 index 0000000..86fa5a2 --- /dev/null +++ b/src/context/deposit.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from 'react' + +import { DepositDto } from '@api' + +/** Контекст текущего месторождения */ +export const DepositContext = createContext(null) + +/** + * Получить текущее месторождение + * + * @returns Текущее месторождение, либо `null` + */ +export const useDeposit = () => useContext(DepositContext) + +/** Контекст со списком месторождений */ +export const DepositListContext = createContext([]) + +/** + * Получить список скважин + * + * @returns Список скважин + */ +export const useDepositList = () => useContext(DepositListContext) diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..34fcba9 --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,5 @@ +export * from './deposit' +export * from './layout_props' +export * from './root_path' +export * from './user' +export * from './well' diff --git a/src/context/layout_props.ts b/src/context/layout_props.ts new file mode 100644 index 0000000..cd07ff3 --- /dev/null +++ b/src/context/layout_props.ts @@ -0,0 +1,31 @@ +import { createContext, useContext, useEffect } from 'react' + +import { LayoutPortalProps } from '@components/LayoutPortal' + +/** Контекст метода редактирования параметров заголовка и меню */ +export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {}) + +/** + * Получить метод задания параметров заголовка и меню + * + * @returns Получить метод задания параметров заголовка и меню + */ +export const useLayoutProps = (props?: LayoutPortalProps) => { + const setLayoutProps = useContext(LayoutPropsContext) + + useEffect(() => { + if (props) setLayoutProps(props) + }, [setLayoutProps, props]) + + return setLayoutProps +} + +/** Контекст для блока справа от крошек на страницах скважин и админки */ +export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {}) + +/** + * Получить метод задания элементов справа от крошек + * + * @returns Метод задания элементов справа от крошек + */ +export const useTopRightBlock = () => useContext(TopRightBlockContext) diff --git a/src/context/root_path.ts b/src/context/root_path.ts new file mode 100644 index 0000000..6c03374 --- /dev/null +++ b/src/context/root_path.ts @@ -0,0 +1,11 @@ +import { createContext, useContext } from 'react' + +/** Контекст текущего корневого пути */ +export const RootPathContext = createContext('/') + +/** + * Получить текущий корневой путь + * + * @returns Текущий корневой путь + */ +export const useRootPath = () => useContext(RootPathContext) diff --git a/src/context/user.ts b/src/context/user.ts new file mode 100644 index 0000000..671d565 --- /dev/null +++ b/src/context/user.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react' + +import { UserTokenDto } from '@api' + +/** Контекст текущего пользователя */ +export const UserContext = createContext({}) + +/** + * Получить текущего пользователя + * + * @returns Текущий пользователь, либо `null` + */ +export const useUser = () => useContext(UserContext) diff --git a/src/context/well.ts b/src/context/well.ts new file mode 100644 index 0000000..54a2075 --- /dev/null +++ b/src/context/well.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react' + +import { WellDto } from '@api' + +/** Контекст текущей скважины */ +export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}]) + +/** + * Получить текущую скважину + * + * @returns Текущая скважина, либо пустой объект + */ +export const useWell = () => useContext(WellContext) diff --git a/src/images/AsbLogo.svg b/src/images/AsbLogo.svg new file mode 100644 index 0000000..1707292 --- /dev/null +++ b/src/images/AsbLogo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/Logo.svg b/src/images/Logo.svg new file mode 100644 index 0000000..dbafbae --- /dev/null +++ b/src/images/Logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/Logo.tsx b/src/images/Logo.tsx index 2dd724b..32f9634 100755 --- a/src/images/Logo.tsx +++ b/src/images/Logo.tsx @@ -1,44 +1,14 @@ import { memo } from 'react' +import { ReactComponent as RawLogo } from './Logo.svg' + export type LogoProps = React.SVGProps & { size?: number onlyIcon?: boolean } export const Logo = memo(({ size = 170, onlyIcon, ...props }) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + )) export default Logo diff --git a/src/index.tsx b/src/index.tsx index 5354f6e..12aaff2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,28 @@ -import React from 'react' +import locale from 'antd/lib/locale/ru_RU' +import { ConfigProvider } from 'antd' import { createRoot } from 'react-dom/client' +import React from 'react' + +import { getUser } from '@utils' +import { OpenAPI } from '@api' import App from './App' +import '@styles/include/antd_theme.less' import '@styles/index.css' +// OpenAPI.BASE = 'http://localhost:3000' +// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости +OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || '' +OpenAPI.HEADERS = { 'Content-Type': 'application/json' } + const container = document.getElementById('root') ?? document.body const root = createRoot(container) root.render( - + + + ) diff --git a/src/pages/AdminPanel/AdminNavigationMenu.jsx b/src/pages/AdminPanel/AdminNavigationMenu.jsx index 0474af2..98a098a 100644 --- a/src/pages/AdminPanel/AdminNavigationMenu.jsx +++ b/src/pages/AdminPanel/AdminNavigationMenu.jsx @@ -12,7 +12,7 @@ import { UserOutlined, } from '@ant-design/icons' -import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' +import { makeItem, PrivateMenu } from '@components/PrivateMenu' import { isDev } from '@utils' export const menuItems = [ @@ -33,7 +33,7 @@ export const menuItems = [ ].filter(Boolean) export const AdminNavigationMenu = memo((props) => ( - { const clusterColumns = useMemo(() => [ makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { width: 200, - editable: true, sorter: makeStringSorter('idDeposit') }), - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), ], [deposits]) diff --git a/src/pages/AdminPanel/CompanyController.jsx b/src/pages/AdminPanel/CompanyController.jsx index 454b16c..5577e1f 100755 --- a/src/pages/AdminPanel/CompanyController.jsx +++ b/src/pages/AdminPanel/CompanyController.jsx @@ -3,10 +3,9 @@ import { Input } from 'antd' import { EditableTable, - makeColumn, - makeStringSorter, makeSelectColumn, - defaultPagination + defaultPagination, + makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminCompanyService, AdminCompanyTypeService } from '@api' @@ -37,16 +36,8 @@ const CompanyController = memo(() => { })) setColumns([ - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), - makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { - width: 200, - editable: true - }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { width: 200 }), ]) await updateTable() diff --git a/src/pages/AdminPanel/CompanyTypeController.jsx b/src/pages/AdminPanel/CompanyTypeController.jsx index 3debe2c..601d1aa 100755 --- a/src/pages/AdminPanel/CompanyTypeController.jsx +++ b/src/pages/AdminPanel/CompanyTypeController.jsx @@ -1,19 +1,14 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' -import { EditableTable, makeColumn, makeStringSorter, defaultPagination } from '@components/Table' +import { EditableTable, defaultPagination, makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { arrayOrDefault, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminCompanyTypeService } from '@api' const columns = [ - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), ] const CompanyTypeController = memo(() => { diff --git a/src/pages/AdminPanel/DepositController.jsx b/src/pages/AdminPanel/DepositController.jsx index 748490b..974a420 100755 --- a/src/pages/AdminPanel/DepositController.jsx +++ b/src/pages/AdminPanel/DepositController.jsx @@ -2,15 +2,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' import { invokeWebApiWrapperAsync } from '@components/factory' -import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table' +import { EditableTable, defaultPagination, makeTimezoneColumn, makeTextColumn, makeNumericColumn } from '@components/Table' import { arrayOrDefault, coordsFormat, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminDepositService } from '@api' const depositColumns = [ - makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), ] diff --git a/src/pages/AdminPanel/PermissionController.jsx b/src/pages/AdminPanel/PermissionController.jsx index 73db56a..0ee736e 100755 --- a/src/pages/AdminPanel/PermissionController.jsx +++ b/src/pages/AdminPanel/PermissionController.jsx @@ -1,23 +1,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' -import { EditableTable, makeColumn, makeStringSorter } from '@components/Table' +import { EditableTable, makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { arrayOrDefault, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminPermissionService } from '@api' const columns = [ - makeColumn('Название', 'name', { - editable: true, - sorter: makeStringSorter('name'), - isRequired: true, - formItemRules: min1, - }), - makeColumn('Описание', 'description', { - editable: true, - sorter: makeStringSorter('description'), - }), + makeTextColumn('Название', 'name', undefined, undefined, undefined, { isRequired: true, formItemRules: min1 }), + makeTextColumn('Описание', 'description'), ] const PermissionController = memo(() => { diff --git a/src/pages/AdminPanel/RoleController.jsx b/src/pages/AdminPanel/RoleController.jsx index b995fde..6276ac5 100755 --- a/src/pages/AdminPanel/RoleController.jsx +++ b/src/pages/AdminPanel/RoleController.jsx @@ -19,15 +19,13 @@ const RoleController = memo(() => { )), [roles, searchValue]) const columns = useMemo(() => [ - makeTextColumn('Название', 'caption', null, null, null, { width: 100, editable: true, formItemRules: min1 }), + makeTextColumn('Название', 'caption', null, null, null, { width: 100, formItemRules: min1 }), makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { width: 400, - editable: true, render: (role) => }, { allowClear: true }), makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { width: 600, - editable: true, render: (permission) => , }), ], [roles, permissions]) diff --git a/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx index f90ff30..3836aff 100755 --- a/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx +++ b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx @@ -50,7 +50,7 @@ const TelemetryController = memo(() => { const columns = useMemo(() => [ makeColumn('', 'hasParent', { render: mergeRender }), - makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), + makeNumericColumn('ID', 'id', makeNumericRender(0)), makeTextColumn('UID', 'remoteUid'), makeTextColumn('Назначена на скважину', 'realWell'), makeDateColumn('Дата начала бурения', 'drillingStartDate'), diff --git a/src/pages/AdminPanel/UserController/index.jsx b/src/pages/AdminPanel/UserController/index.jsx index 2f6b301..5bb3ef2 100755 --- a/src/pages/AdminPanel/UserController/index.jsx +++ b/src/pages/AdminPanel/UserController/index.jsx @@ -115,7 +115,6 @@ const UserController = memo(() => { setColumns([ makeTextColumn('Логин', 'login', null, null, null, { - editable: true, formItemRules: [ { required: true }, ...createLoginRules, @@ -130,41 +129,34 @@ const UserController = memo(() => { ], }), makeTextColumn('Фамилия', 'surname', filters.surname, null, null, { - editable: true, formItemRules: [{ required: true }, ...nameRules], filterSearch: true, onFilter: makeTextOnFilter('surname'), }), makeTextColumn('Имя', 'name', filters.name, null, null, { - editable: true, formItemRules: nameRules, filterSearch: true, onFilter: makeTextOnFilter('name'), }), makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, { - editable: true, formItemRules: nameRules, filterSearch: true, onFilter: makeTextOnFilter('patronymic'), }), makeTextColumn('E-mail', 'email', filters.email, null, null, { - editable: true, formItemRules: [{ required: true }, ...emailRules], filterSearch: true, onFilter: makeTextOnFilter('email'), }), makeTextColumn('Номер телефона', 'phone', null, null, null, { - editable: true, formItemRules: phoneRules, }), - makeTextColumn('Должность', 'position', null, null, null, { editable: true }), + makeTextColumn('Должность', 'position', null, null, null), makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { - editable: true, input: , onFilter: makeArrayOnFilter('roleNames'), }), makeSelectColumn('Компания', 'idCompany', companies, '--', { - editable: true, sorter: makeNumericSorter('idCompany'), }) ]) diff --git a/src/pages/AdminPanel/WellController/index.jsx b/src/pages/AdminPanel/WellController/index.jsx index 6fe163e..ed74ca6 100755 --- a/src/pages/AdminPanel/WellController/index.jsx +++ b/src/pages/AdminPanel/WellController/index.jsx @@ -12,11 +12,12 @@ import { EditableTable, makeColumn, makeSelectColumn, - makeStringSorter, makeNumericSorter, makeTagColumn, defaultPagination, makeTimezoneColumn, + makeTextColumn, + makeNumericColumn, } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { TelemetryView, CompanyView } from '@components/views' @@ -81,23 +82,11 @@ const WellController = memo(() => { })) setColumns([ - makeSelectColumn('Куст', 'idCluster', clusters, '--', { - width: '5rem', - editable: true, - sorter: makeNumericSorter('idCluster'), - }), - makeColumn('Название', 'caption', { - width: '5rem', - editable: true, - sorter: makeStringSorter('caption'), - }), - makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { - width: 150, - editable: true, - sorter: makeNumericSorter('idWellType'), - }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeSelectColumn('Куст', 'idCluster', clusters, '--', { width: '5rem', sorter: makeNumericSorter('idCluster') }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: '5rem' }), + makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { width: 150, sorter: makeNumericSorter('idWellType') }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeColumn('Телеметрия', 'telemetry', { editable: true, render: (telemetry) => , @@ -105,7 +94,6 @@ const WellController = memo(() => { }, ), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { - editable: true, render: (company) => , }), ]) diff --git a/src/pages/AdminPanel/index.jsx b/src/pages/AdminPanel/index.jsx index e4c11b8..98c8e65 100755 --- a/src/pages/AdminPanel/index.jsx +++ b/src/pages/AdminPanel/index.jsx @@ -1,9 +1,9 @@ -import { Navigate, Route, Routes, useLocation } from 'react-router-dom' +import { Navigate, Route, Routes } from 'react-router-dom' import { lazy, memo, useEffect, useMemo } from 'react' import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context' import { FastRunMenu } from '@components/FastRunMenu' -import { makeMenuBreadcrumbItems } from '@components/MenuBreadcrumb' +import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb' import { NoAccessComponent, withPermissions } from '@utils' import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu' @@ -21,21 +21,18 @@ const TelemetryViewer = lazy(() => import('./Telemetry/TelemetryViewer')) const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger')) const VisitLog = lazy(() => import('./VisitLog')) +const layoutProps = { + sider: , + title: 'Администраторская панель', + isAdmin: true, + breadcrumb: makeMenuBreadcrumbItemsRender(menuItems, /^\/admin\//), +} + const AdminPanel = memo(() => { - const location = useLocation() const root = useRootPath() const rootPath = useMemo(() => `${root}/admin`, [root]) - const setLayoutProps = useLayoutProps() - - useEffect(() => { - setLayoutProps({ - sider: , - title: 'Администраторская панель', - isAdmin: true, - breadcrumb: makeMenuBreadcrumbItems(menuItems, location.pathname, /^\/admin\//), - }) - }, [location.pathname]) + useLayoutProps(layoutProps) return ( diff --git a/src/pages/Cluster/ClusterWells.jsx b/src/pages/Cluster/ClusterWells.jsx index a74c8bd..81410ec 100755 --- a/src/pages/Cluster/ClusterWells.jsx +++ b/src/pages/Cluster/ClusterWells.jsx @@ -7,11 +7,11 @@ import { makeTextColumn, makeGroupColumn, makeColumn, - makeDateSorter, - makeNumericColumnPlanFact, + makeNumericColumnPlanFactOld, Table, makeNumericRender, makeNumericColumn, + makeDateColumn, } from '@components/Table' import LoaderPortal from '@components/LoaderPortal' import PointerIcon from '@components/icons/PointerIcon' @@ -39,7 +39,6 @@ const filtersWellsType = [] const DAY_IN_MS = 86_400_000 const ONLINE_DEADTIME = 600_000 -const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-' const numericRender = makeNumericRender(1) const ClusterWells = memo(({ statsWells }) => { @@ -118,7 +117,10 @@ const ClusterWells = memo(({ statsWells }) => { const columns = useMemo(() => [ makeTextColumn('скв №', 'caption', null, null, (_, item) => ( - + { ), makeTextColumn('Тип скв.', 'wellType', filtersWellsType, null, (text) => text ?? '-'), makeGroupColumn('Фактические сроки', [ - makeColumn('начало', 'factStart', { sorter: makeDateSorter('factStart'), render: getDate }), - makeColumn('окончание', 'factEnd', { sorter: makeDateSorter('factEnd'), render: getDate }), + makeDateColumn('начало', 'factStart'), + makeDateColumn('окончание', 'factEnd'), ]), - makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', filtersMinMax, makeFilterMinMaxFunction, numericRender), + makeNumericColumnPlanFactOld('Продолжительность, сут', 'period', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('МСП, м/ч', 'rateOfPenetration', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'routeSpeed', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => (
- - - ) -}) - -export default withPermissions(Deposit, ['Cluster.get']) diff --git a/src/pages/Deposit/DepositNavigationMenu.jsx b/src/pages/Deposit/DepositNavigationMenu.jsx new file mode 100644 index 0000000..af138bc --- /dev/null +++ b/src/pages/Deposit/DepositNavigationMenu.jsx @@ -0,0 +1,25 @@ +import { memo } from 'react' +import { + FundOutlined, + HeatMapOutlined, +} from '@ant-design/icons' + +import { makeItem, PrivateMenu } from '@components/PrivateMenu' + +export const menuItems = [ + makeItem('Карта', 'map', [], ), + makeItem('Наработка АКБ', 'statistics_adw', [], ), +] + +export const DepositNavigationMenu = memo((props) => ( + +)) + +export default DepositNavigationMenu diff --git a/src/pages/Deposit/Map.jsx b/src/pages/Deposit/Map.jsx new file mode 100644 index 0000000..911c137 --- /dev/null +++ b/src/pages/Deposit/Map.jsx @@ -0,0 +1,93 @@ +import { memo, useMemo, useCallback } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { Map as PigeonMap, Overlay } from 'pigeon-maps' +import { Popover, Badge } from 'antd' + +import { useDepositList } from '@asb/context' +import { PointerIcon } from '@components/icons' +import { limitValue, withPermissions } from '@utils' + +import '@styles/index.css' + +const zoomLimit = limitValue(5, 15) + +const calcViewParams = (clusters) => { + if ((clusters?.length ?? 0) <= 0) + return { center: [60.81226, 70.0562], zoom: 5 } + + const center = clusters.reduce((sum, cluster) => { + sum[0] += cluster.latitude + sum[1] += cluster.longitude + return sum + }, [0, 0]).map((elm) => elm / clusters.length) + + const maxDeg = clusters.reduce((max, cluster) => { + const dLatitude = Math.abs(center[0] - cluster.latitude) + const dLongitude = Math.abs(center[1] - cluster.longitude) + return Math.max(Math.max(dLatitude, dLongitude), max) + }, 0) + + // zoom max = 20 (too close) + // zoom min = 1 (mega far) + // 4 - full Russia (161.6 deg) + // 13.5 - Khanty-Mansiysk + const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5)) + + return { center, zoom } +} + +const Map = memo(() => { + const deposits = useDepositList() + const location = useLocation() + + const makeDepositLinks = useCallback((clusters) => ( +
+ {clusters.map(cluster => ( + +
{cluster.caption}
+ + ))} +
+ ), [location.pathname]) + + const viewParams = useMemo(() => calcViewParams(deposits), [deposits]) + + return ( +
+ + {deposits.map(deposit => { + const anchor = [deposit.latitude, deposit.longitude] + const links = makeDepositLinks(deposit.clusters) + + return ( + + + {deposit.caption} + + )} + > +
+ + + +
+
+
+ ) + })} +
+
+ ) +}) + +export default withPermissions(Map, ['Cluster.get']) diff --git a/src/pages/Deposit/StatisticsADW.jsx b/src/pages/Deposit/StatisticsADW.jsx new file mode 100644 index 0000000..d1a17bb --- /dev/null +++ b/src/pages/Deposit/StatisticsADW.jsx @@ -0,0 +1,127 @@ +import { CheckOutlined, StopOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons' +import { memo, useEffect, useMemo, useState } from 'react' +import { Card, Empty, Popover } from 'antd' + +import LoaderPortal from '@components/LoaderPortal' +import { getWellTitle, WellView } from '@components/views' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { DateRangeWrapper, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' +import { arrayOrDefault, withPermissions } from '@utils' +import { SubsystemOperationTimeService } from '@api' + +import '@styles/pages/statistics_adw.less' + +const numericRender = makeNumericRender(2) + +const columns = [ + makeTextColumn('Подсистема', 'subsystemName', undefined, undefined, (value, row) => value || row.key), + makeNumericColumn('Проходка, м', 'sumDepthInterval'), + makeNumericColumn('Время работы, ч', 'usedTimeHours'), + makeNumericColumn('Кол-во запусков', 'operationCount'), + makeNumericColumn('Коэф. использования, %', 'kUsage', (value) => numericRender(value * 100)), +] + +const getSubsystemState = (subsystem) => { + if (!subsystem || typeof subsystem.kUsage !== 'number') return null + if (subsystem.kUsage <= 0.2) return 'error' + if (subsystem.kUsage <= 0.7) return 'warn' + return 'ok' +} + +const getSubsystemIcon = (state) => { + switch (state) { + case 'ok': return + case 'warn': return + case 'error': return + default: return + } +} + +const getCardState = (subsystems) => { + if (subsystems.length <= 0) return null + const states = subsystems.map(getSubsystemState) + if (states.some((state) => state === 'error')) return 'error' + if (states.some((state) => state === 'warn')) return 'warn' + return 'ok' +} + +const generateSubsystem = (subsystem) => { + const state = getSubsystemState(subsystem) + + return ( +
+ {getSubsystemIcon(state)} + {subsystem.subsystemName || subsystem.key} +
+ ) +} + +const onRow = (record) => ({ className: `status-${getSubsystemState(record)}` }) +const objectToArray = (obj) => Object.entries(obj).filter(([_, v]) => v).map(([key, v]) => ({ key, ...v })) + +const GeneralSubsystemStatistics = memo(() => { + const [isLoading, setIsLoading] = useState(false) + const [dates, setDates] = useState([null, null]) + const [data, setData] = useState([]) + + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + const data = await SubsystemOperationTimeService.getStatByWell(dates?.[0]?.toISOString(), dates?.[1]?.toISOString()) + const out = arrayOrDefault(data).map(({ well, ...ss }) => ({ well, subsystems: objectToArray(ss) })) + setData(out) + }, + setIsLoading, + 'Не удалось загрузить статистику наработки подсистем', + { actionName: 'Загрузка статистики наработки подсистем' }, + ) + }, [dates]) + + const cards = useMemo(() => data.map((row) => { + const state = getCardState(row.subsystems) + + const card = ( + +
+ {state ? row.subsystems.map((ss) => generateSubsystem(ss)) : } +
+
+ ) + + if (!state) return card + + return ( + + Детальная информация по скважине + + + )} + content={( + + )} + >{card} + ) + }), [data]) + + return ( + +
+
+ Диапазон дат: + +
+
{cards}
+
+
+ ) +}) + +export default withPermissions(GeneralSubsystemStatistics, []) diff --git a/src/pages/Deposit/index.jsx b/src/pages/Deposit/index.jsx new file mode 100644 index 0000000..a4cadf6 --- /dev/null +++ b/src/pages/Deposit/index.jsx @@ -0,0 +1,75 @@ +import { Navigate, Route, Routes, useParams } from 'react-router-dom' +import { lazy, memo, useEffect, useMemo } from 'react' + +import { DepositContext, RootPathContext, useDepositList, useLayoutProps, useRootPath } from '@asb/context' +import FastRunMenu from '@components/FastRunMenu' +import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb' +import { NoAccessComponent, withPermissions } from '@utils' + +import { DepositNavigationMenu, menuItems } from './DepositNavigationMenu' + +const Map = lazy(() => import('./Map')) +const StatisticsADW = lazy(() => import('./StatisticsADW')) + +const breadcrumb = makeMenuBreadcrumbItemsRender(menuItems, /^\/deposit\/[^\/#?]+\//) + +const Deposit = memo(() => { + const { '*': param } = useParams() + + const setLayoutProps = useLayoutProps() + const deposits = useDepositList() + + const root = useRootPath() + const rootPath = useMemo(() => `${root}/deposit`, [root]) + + const [idDeposit, isMap] = useMemo(() => { + const result = /^([^\/#?]+)(:?\/([^\/#?]+))?/.exec(param) + if (!result) return [null, false] + console.log(result) + return [ + result[1] !== 'null' ? Number(result[1]) : null, + result[3] === 'map', + ] + }, [param]) + + const deposit = useMemo(() => deposits.find((deposit) => deposit.id === idDeposit) || null, [deposits, idDeposit]) + + useEffect(() => { + const key = idDeposit ? `/deposit/${idDeposit}` : null + + const selectorProps = { + expand: key ? [key] : true, + current: key || undefined, + } + + setLayoutProps({ + breadcrumb: !isMap && breadcrumb, + sheet: !isMap, + sider: , + showSelector: isMap, + selectorProps, + title: 'Месторождение', + }) + }, [setLayoutProps, idDeposit, isMap]) + + return ( + + + + + } /> + + + } /> + } /> + + } /> + } /> + + + + + ) +}) + +export default withPermissions(Deposit, []) diff --git a/src/pages/Well/Analytics/Statistics.jsx b/src/pages/Well/Analytics/Statistics.jsx index 68d4244..5633bd2 100644 --- a/src/pages/Well/Analytics/Statistics.jsx +++ b/src/pages/Well/Analytics/Statistics.jsx @@ -10,7 +10,7 @@ import { OperationStatService, WellOperationService } from '@api' import { arrayOrDefault, withPermissions } from '@utils' import '@styles/index.css' -import '@styles/statistics.less' +import '@styles/pages/statistics.less' const { Text } = Typography const { Summary } = RawTable @@ -21,13 +21,13 @@ const speedNumericRender = (section) => numericRender(section?.speed) const makeSectionSorter = (key, name) => (a, b) => (a?.[key]?.[name] ?? 0) - (b?.[key]?.[name] ?? 0) export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [ - makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100, { + makeNumericColumn('Проходка', key, (section => numericRender(section?.depth)), undefined, 100, { sorter: makeSectionSorter(key, 'depth'), }), - makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100, { + makeNumericColumn('Время', key, (section => numericRender(section?.time)), undefined, 100, { sorter: makeSectionSorter(key, 'time'), }), - makeNumericColumn((<>Vрейсовая), key, null, null, speedRender ?? speedNumericRender, 100, { + makeNumericColumn((<>Vрейсовая), key, speedRender ?? speedNumericRender, undefined, 100, { sorter: makeSectionSorter(key, 'speed'), }), ]) @@ -37,7 +37,7 @@ export const defaultColumns = [ makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }), ] -const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: 200 } +const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: '25vh' } const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2 const getWellData = async (wellsList) => { diff --git a/src/pages/Well/Analytics/WellCompositeEditor/NewParamsTable.jsx b/src/pages/Well/Analytics/WellCompositeEditor/NewParamsTable.jsx deleted file mode 100644 index 9c3d812..0000000 --- a/src/pages/Well/Analytics/WellCompositeEditor/NewParamsTable.jsx +++ /dev/null @@ -1,146 +0,0 @@ -import { memo, useCallback, useEffect, useState } from 'react' -import { Button, Modal, Popconfirm } from 'antd' - -import { useWell } from '@asb/context' -import { makeColumn, makeGroupColumn, makeNumericRender, makeSelectColumn, Table } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' -import { invokeWebApiWrapperAsync } from '@components/factory' -import { DrillParamsService, WellOperationService } from '@api' - -const getDeepValue = (data, key) => { - if (!key || key.trim() === '') return null - const keys = key.split('.') - let out = data - while (keys.length > 0) { - if (!(keys[0] in out)) return null - out = out[keys[0]] - keys.splice(0, 1) - } - return out -} - -const makeNumericSorter = (keys) => (a, b) => getDeepValue(a, keys) - getDeepValue(b, keys) - -const numericRender = makeNumericRender(1) -const makeNumericColumn = (title, dataIndex, render, other) => makeColumn(title, dataIndex, { - sorter: makeNumericSorter(dataIndex), - render: (_, record, index) => { - const func = render ?? ((value) => <>{value}) - const item = getDeepValue(record, dataIndex) - return func(item, record, index) - }, - align: 'right', - ...other, -}) - -const makeAvgRender = (dataIndex) => (avg, record) => { - const max = record[dataIndex]?.max - const fillW = (max - avg) / max * 100 - return ( -
-
-
- {numericRender(avg)} -
-
- ) -} - -const makeNumericAvgRange = (title, dataIndex, defaultRender = false) => makeGroupColumn(title, [ - makeNumericColumn('мин', `${dataIndex}.min`), - makeNumericColumn('сред', `${dataIndex}.avg`, defaultRender ? undefined : makeAvgRender(dataIndex)), - makeNumericColumn('макс', `${dataIndex}.max`), -]) - -export const getColumns = async (idWell) => { - let sectionTypes = await WellOperationService.getSectionTypes(idWell) - sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({ - label: value, - value: id, - })) - - return [ - makeSelectColumn('Конструкция секции','idWellSectionType', sectionTypes, null, { - width: 160, - sorter: makeNumericSorter('idWellSectionType'), - }), - makeNumericAvgRange('Нагрузка, т', 'axialLoad'), - makeNumericAvgRange('Давление, атм', 'pressure'), - makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', true), - makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed'), - makeNumericAvgRange('Расход, л/с', 'flow'), - ] -} - -export const NewParamsTable = memo(({ selectedWellsKeys }) => { - const [params, setParams] = useState([]) - const [paramsColumns, setParamsColumns] = useState([]) - const [showParamsLoader, setShowParamsLoader] = useState(false) - const [isParamsModalVisible, setIsParamsModalVisible] = useState(false) - - const [well] = useWell() - - useEffect(() => { - invokeWebApiWrapperAsync(async () => setParamsColumns(await getColumns(well.id))) - }, [well]) - - const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync( - async () => { - setIsParamsModalVisible(true) - const params = await DrillParamsService.getCompositeAll(well.id) - setParams(params) - }, - setShowParamsLoader, - `Не удалось загрузить список режимов`, - { actionName: 'Получение списка режимов скважины', well } - ), [well]) - - const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync( - async () => { - await DrillParamsService.save(well.id, params) - setIsParamsModalVisible(false) - }, - setShowParamsLoader, - `Не удалось добавить режимы в список`, - { actionName: 'Добавление режима скважины', well } - ), [well, params]) - - return ( - <> - - setIsParamsModalVisible(false)} - width={1700} - footer={( - - - - )} - > - -
- - - - ) -}) - -export default NewParamsTable diff --git a/src/pages/Well/Analytics/WellCompositeEditor/WellCompositeSections.jsx b/src/pages/Well/Analytics/WellCompositeEditor/WellCompositeSections.jsx index 426ed21..9747b6c 100644 --- a/src/pages/Well/Analytics/WellCompositeEditor/WellCompositeSections.jsx +++ b/src/pages/Well/Analytics/WellCompositeEditor/WellCompositeSections.jsx @@ -1,12 +1,13 @@ import { Link, useLocation } from 'react-router-dom' import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react' import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons' -import { Table, Button, Badge, Divider, Modal, Row, Col } from 'antd' +import { Button, Badge, Divider, Modal } from 'antd' import { useWell } from '@asb/context' import LoaderPortal from '@components/LoaderPortal' +import SuspenseFallback from '@components/SuspenseFallback' import { invokeWebApiWrapperAsync } from '@components/factory' -import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table' +import { Table, makeTextColumn, makeNumericColumnPlanFactOld, makeNumericColumn } from '@components/Table' import { WellCompositeService } from '@api' import { hasPermission, @@ -16,8 +17,6 @@ import { getOperations } from '@utils' -import NewParamsTable from './NewParamsTable' -import SuspenseFallback from '@asb/components/SuspenseFallback' const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd')) const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable')) @@ -146,15 +145,15 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => { makeTextColumn('скв №', 'caption', null, null, (text, item) => {text ?? '-'} ), - makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-'), - makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('Продолжительность, ч', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('МСП, м/ч', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumnPlanFact('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction), - makeNumericColumn('НПВ, ч', 'nonProductiveHours', filtersMinMax, makeFilterMinMaxFunction, null, '80px'), + makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-', { width: 100 }), + makeNumericColumnPlanFactOld('Глубина, м', 'sectionWellDepth', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Продолжительность, ч', 'sectionBuildDays', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('МСП, м/ч', 'sectionRateOfPenetration', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'sectionRouteSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), + makeNumericColumn('НПВ, ч', 'nonProductiveHours', undefined, makeFilterMinMaxFunction, '80px', { filters: filtersMinMax }), { title: 'TVD', render: (value) => ( @@ -200,11 +199,11 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => { dataSource={rows} size={'small'} bordered - scroll={{ x: true, y: 620 }} + scroll={{ x: true, y: '30vh' }} rowSelection={rowSelection} pagination={false} /> - +

Выбранные секции

@@ -214,12 +213,9 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => { rowSelection={rowSelection} size={'small'} bordered - scroll={{ x: true }} + scroll={{ x: true, y: '30vh' }} pagination={false} /> - -
- import('@pages/Cluster/ClusterWells')) diff --git a/src/pages/Well/Documents/DocumentsTemplate.jsx b/src/pages/Well/Documents/DocumentsTemplate.jsx index 673a8a2..69d0b74 100644 --- a/src/pages/Well/Documents/DocumentsTemplate.jsx +++ b/src/pages/Well/Documents/DocumentsTemplate.jsx @@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head ), }, makeDateColumn('Дата загрузки', 'uploadDate'), - makeNumericColumn('Размер', 'size', null, null, (value) => formatBytes(value)), + makeNumericColumn('Размер', 'size', (value) => formatBytes(value)), makeColumn('Автор', 'author', { render: item => }), makeColumn('Компания', 'company', { render: (_, record) => }), ...(customColumns ?? []) diff --git a/src/pages/Well/DrillingProgram/CategoryAdder.jsx b/src/pages/Well/DrillingProgram/CategoryAdder.jsx index bdecf0d..2c95341 100644 --- a/src/pages/Well/DrillingProgram/CategoryAdder.jsx +++ b/src/pages/Well/DrillingProgram/CategoryAdder.jsx @@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import { DrillingProgramService } from '@api' -import '@styles/drilling_program.less' +import '@styles/pages/drilling_program.less' const catSelectorRules = [{ required: true, diff --git a/src/pages/Well/DrillingProgram/CategoryHistory.jsx b/src/pages/Well/DrillingProgram/CategoryHistory.jsx index 5272d7a..f1605da 100644 --- a/src/pages/Well/DrillingProgram/CategoryHistory.jsx +++ b/src/pages/Well/DrillingProgram/CategoryHistory.jsx @@ -12,7 +12,7 @@ import { FileService } from '@api' import MarksCard from './MarksCard' -import '@styles/drilling_program.less' +import '@styles/pages/drilling_program.less' const { RangePicker } = DatePicker const { Search } = Input @@ -74,7 +74,7 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => { const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] const skip = (page - 1) * pageSize - const paginatedHistory = await FileService.getFilesInfo(well.caption, idCategory, companyName, fileName, begin, end, false, skip, pageSize) + const paginatedHistory = await FileService.getFilesInfo(well.id, idCategory, companyName, fileName, begin, end, false, skip, pageSize) setTotal(paginatedHistory?.count ?? 0) setData(arrayOrDefault(paginatedHistory?.items)) }, diff --git a/src/pages/Well/DrillingProgram/CategoryRender.jsx b/src/pages/Well/DrillingProgram/CategoryRender.jsx index db5b2d4..3f21adf 100644 --- a/src/pages/Well/DrillingProgram/CategoryRender.jsx +++ b/src/pages/Well/DrillingProgram/CategoryRender.jsx @@ -18,7 +18,7 @@ import { formatDate, MimeTypes } from '@utils' import MarksCard from './MarksCard' -import '@styles/drilling_program.less' +import '@styles/pages/drilling_program.less' const CommentPrompt = memo(({ isRequired = true, ...props }) => ( cols.map(col => col.map(col => ({ render: renderDelegate, ...col }))) diff --git a/src/pages/Well/Measure/View.jsx b/src/pages/Well/Measure/View.jsx index 5f1b24d..6f61143 100644 --- a/src/pages/Well/Measure/View.jsx +++ b/src/pages/Well/Measure/View.jsx @@ -4,13 +4,13 @@ import { Empty, Form } from 'antd' import { Grid, GridItem } from '@components/Grid' import '@styles/index.css' -import '@styles/measure.css' +import '@styles/pages/measure.css' export const View = memo(({ columns, item }) => !item || !columns?.length ? ( ) : ( - {columns.map((cols, i) => { + {columns.flatMap((cols, i) => { const columnPosition = 1 + i * 2 return cols.map((column, j) => ( @@ -46,6 +46,6 @@ export const View = memo(({ columns, item }) => !item || !columns?.length ? ( )) - }).flat()} + })} )) diff --git a/src/pages/Well/Measure/columnsCommon.jsx b/src/pages/Well/Measure/columnsCommon.jsx index 8b512bd..1ee6bd4 100644 --- a/src/pages/Well/Measure/columnsCommon.jsx +++ b/src/pages/Well/Measure/columnsCommon.jsx @@ -2,7 +2,7 @@ import { Input } from 'antd' import { RegExpIsFloat } from '@components/Table' -import '@styles/measure.css' +import '@styles/pages/measure.css' export const v = (text) => (
diff --git a/src/pages/Well/Reports/DailyReport/ReportEditor.jsx b/src/pages/Well/Reports/DailyReport/ReportEditor.jsx index e77fc61..05c171a 100644 --- a/src/pages/Well/Reports/DailyReport/ReportEditor.jsx +++ b/src/pages/Well/Reports/DailyReport/ReportEditor.jsx @@ -1,15 +1,15 @@ -import { DatePicker, Descriptions, Form, Input, InputNumber, Modal, Table, Tabs } from 'antd' +import { DatePicker, Descriptions, Form, Input, InputNumber, Modal, Table as RawTable, Tabs } from 'antd' import { memo, useCallback, useEffect, useState } from 'react' import moment from 'moment' import { useWell } from '@asb/context' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' -import { makeColumn, makeGroupColumn } from '@components/Table' +import { Table, makeColumn, makeGroupColumn } from '@components/Table' import { DailyReportService } from '@api' const { Item: RawItem } = Form -const { Summary } = Table +const { Summary } = RawTable const { TabPane } = Tabs const Item = memo(({ style, ...other }) => ) diff --git a/src/pages/Well/Telemetry/Archive/index.jsx b/src/pages/Well/Telemetry/Archive/index.jsx deleted file mode 100644 index b15afdf..0000000 --- a/src/pages/Well/Telemetry/Archive/index.jsx +++ /dev/null @@ -1,268 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { useState, useEffect, memo, useCallback, useMemo } from 'react' -import { useSearchParams } from 'react-router-dom' -import { Select } from 'antd' - -import { useWell } from '@asb/context' -import { Flex } from '@components/Grid' -import { D3MonitoringCharts } from '@components/d3/monitoring' -import { CopyUrlButton } from '@components/CopyUrl' -import LoaderPortal from '@components/LoaderPortal' -import { invokeWebApiWrapperAsync } from '@components/factory' -import { DatePickerWrapper, makeDateSorter } from '@components/Table' -import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' -import { formatDate, range, withPermissions } from '@utils' -import { TelemetryDataSaubService } from '@api' - -import { makeChartGroups, normalizeData, yAxis } from '../TelemetryView' -import cursorRender from '../TelemetryView/cursorRender' - -const DATA_COUNT = 2048 // Колличество точек на подгрузку графика -const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков -const LOADING_TRIGGER = 0.5 - -const scrollOptions = [ - { label: '10%', value: 0.1 }, - { label: '15%', value: 0.15 }, - { label: '25%', value: 0.25 }, -] - -const getLoadingInterval = (loaded, startDate, interval) => { - // Если данные загружены и дата не заходит за тригер дозагрузка не требуется - if ( - loaded && - +startDate - interval * LOADING_TRIGGER > loaded.start && - +startDate + interval * (LOADING_TRIGGER + 1) < loaded.end - ) - return { loadingStartDate: startDate, newLoaded: loaded, loadingInterval: 0 } - - let loadingStartDate = +startDate - interval * ADDITIVE_PAGES - let loadingEndDate = +startDate + interval * (ADDITIVE_PAGES + 1) - - const newLoaded = { - start: loadingStartDate, - end: loadingEndDate - } - - if (loaded) { - if (loadingStartDate >= loaded.start) - loadingStartDate = loaded.end - if (loadingEndDate <= loaded.end) - loadingEndDate = loaded.start - newLoaded.start = Math.min(loaded.start, loadingStartDate) - newLoaded.end = Math.max(loaded.end, loadingEndDate) - } - - const loadingInterval = Math.trunc((loadingEndDate - loadingStartDate) / 1000) - - return { - loadingStartDate: new Date(loadingStartDate), - newLoaded: { - start: new Date(newLoaded.start), - end: new Date(newLoaded.end) - }, - loadingInterval - } -} - -const interpolationSearch = (data, begin, end, accessor) => { - const fy = (i) => new Date(data[i]?.[accessor] ?? 0) - const fx = (y, b, e) => Math.round(b + (y - fy(b)) * (e - b) / (fy(e) - fy(b))) - const findIdx = (val, startIdx, c) => { - let x = startIdx - let endIdx = data.length - 1 - if(val < fy(startIdx)) - return startIdx - if(val > fy(endIdx)) - return endIdx - for(let i = 0; i < c; i++){ - x = fx(val, startIdx, endIdx) - if(fy(x) < val) - startIdx = x - else - endIdx = x - if ((startIdx === endIdx)||(fy(startIdx) === fy(endIdx))) - return x - } - return x - } - let x0 = findIdx(begin, 0, 100) - let x1 = findIdx(end, x0, 100) - return { start: x0, end: x1, count: x1 - x0 } -} - -export const cutData = (data, beginDate, endDate) => { - if (data?.length > 0) { - let { start, end } = interpolationSearch(data, beginDate, endDate, 'date') - if (start > 0) start-- - if (end + 1 < end.length) end++ - return data.slice(start, end) - } - return data -} - -const chartGroups = makeChartGroups([]) - -const Archive = memo(() => { - const [dataSaub, setDataSaub] = useState([]) - const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) - const [showLoader, setShowLoader] = useState(false) - const [loaded, setLoaded] = useState(null) - - const [well] = useWell() - const [search, setSearchParams] = useSearchParams() - - const getInitialRange = useCallback(() => parseInt(search.get('range') ?? defaultPeriod) * 1000, [search]) - - const [scrollPercent, setScrollPercent] = useState(0.15) - const [chartInterval, setChartInterval] = useState(getInitialRange) - const getInitialDate = useCallback(() => new Date(search.get('start') ?? (Date.now() - chartInterval)), [search, chartInterval]) - const [startDate, setStartDate] = useState(getInitialDate) - - const onGraphWheel = useCallback((e) => { - if (loaded && dateLimit.from && dateLimit.to) { - setStartDate((prevStartDate) => { - const offset = e.deltaY / 100 * chartInterval * scrollPercent - const nextStartDate = +prevStartDate + offset - const firstPossibleDate = Math.max(loaded.start, dateLimit.from) - const lastPossibleDate = Math.min(dateLimit.to, (loaded.end ?? Date.now())) - chartInterval - return new Date(Math.max(firstPossibleDate, Math.min(nextStartDate, lastPossibleDate))) - }) - } - }, [loaded, dateLimit, chartInterval, scrollPercent]) - - const isDateDisabled = useCallback((date) => { - if (!date) return false - const dt = new Date(date).setHours(0, 0, 0, 0) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }, [dateLimit, chartInterval]) - - const isDateTimeDisabled = useCallback((date) => ({ - disabledHours: () => range(24).filter(h => { - if (!date) return false - const dt = +new Date(date).setHours(h) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }), - disabledMinutes: () => range(60).filter(m => { - if (!date) return false - const dt = +new Date(date).setMinutes(m) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }), - disabledSeconds: () => range(60).filter(s => { - if (!date) return false - const dt = +new Date(date).setSeconds(s) - return dt < dateLimit.from || dt > +dateLimit.to - chartInterval - }) - }), [dateLimit, chartInterval]) - - useEffect(() => { - const params = {} - if (startDate) - params.start = startDate.toISOString() - if (chartInterval) - params.range = chartInterval / 1000 - setSearchParams(params) - }, [startDate, chartInterval]) - - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - let dates = await TelemetryDataSaubService.getDataDatesRange(well.id) - dates = { - from: new Date(dates?.from ?? 0), - to: new Date(dates?.to ?? 0) - } - setDateLimit(dates) - }, - setShowLoader, - `Не удалось загрузить диапозон телеметрии`, - { actionName: 'Загрузка диапозона телеметрии', well } - ) - }, [well]) - - useEffect(() => { - setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval)))) - }, [chartInterval, dateLimit]) - - useEffect(() => { - if (showLoader) return - const { loadingStartDate, loadingInterval, newLoaded } = getLoadingInterval(loaded, startDate, chartInterval) - if (loadingInterval <= 0) return - invokeWebApiWrapperAsync( - async () => { - const data = await TelemetryDataSaubService.getData(well.id, loadingStartDate.toISOString(), loadingInterval, DATA_COUNT) - - const loadedStartDate = new Date(Math.max(+newLoaded.start, +startDate - chartInterval * ADDITIVE_PAGES)) - const loadedEndDate = new Date(Math.min(+newLoaded.end, +startDate + chartInterval * (ADDITIVE_PAGES + 1))) - setLoaded({ start: loadedStartDate, end: loadedEndDate }) - - if (data) { - data.forEach(elm => elm.date = new Date(elm.date)) - setDataSaub((prevDataSaub) => { - const newData = [...prevDataSaub, ...normalizeData(data)] - newData.sort(makeDateSorter('date')) - return cutData(newData, loadedStartDate, loadedEndDate) - }) - } - - }, - setShowLoader, - `Не удалось загрузить данные c ${formatDate(startDate)} по ${formatDate(+startDate + chartInterval)}`, - { actionName: 'Загрузка телеметрий в диапозоне', well } - ) - }, [well, chartInterval, loaded, startDate]) - - const onRangeChange = useCallback((value) => { - setChartInterval(value * 1000) - setDataSaub([]) - setLoaded(null) - }, []) - - const domain = useMemo(() => ({ min: startDate, max: new Date(+startDate + chartInterval)}), [startDate, chartInterval]) - const chartData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain]) - - return ( - - -
- Начальная дата:  - setStartDate(new Date(startDate))} - /> -
-
- Период:  - -
-
- Прокрутка:  - , - render: (val) => setpointNames.find((name) => name.value === val)?.label - }, { - title: 'Значение', - dataIndex: 'value', - editable: true, - isRequired: true, - width: 125, - input: `${value}`.replace(',', '.')}/>, - render: makeNumericRender(1), - align: 'right' - } + makeSelectColumn('Наименование уставки', 'name', setpointNames, undefined, { width: 200, isRequired: true }), + makeNumericColumn('Значение', 'value', makeNumericRender(1), undefined, 125, { isRequired: true, align: 'right' }), ], [setpointNames]) const onAdd = useCallback(async (sp) => setSetpoints((prevSp) => { diff --git a/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx b/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx new file mode 100644 index 0000000..fec6b1a --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx @@ -0,0 +1,92 @@ +import { isValidElement, memo, useEffect, useMemo, useState } from 'react' +import { CaretUpOutlined, CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons' +import { Tooltip } from 'antd' + +import { formatDate, isRawDate } from '@utils' + +import '@styles/components/data_summary.less' + +export const parseValue = (value, formatter) => { + if (!value || String(value).trim().length <= 0) return '---' + if (typeof formatter === 'function') return formatter(value) + const v = +value + if (Number.isFinite(v)) + return Number.isInteger(formatter) ? v.toFixed(formatter) : v.toPrecision(4) + if (isRawDate(value)) return formatDate(value) + return value +} + +export const DashboardDisplay = memo(({ label, title, unit, iconRenderer, value, format }) => { + const [icon, setIcon] = useState(null) + + const val = useMemo(() => parseValue(value, format), [value, format]) + + useEffect(() => setIcon((prev) => iconRenderer?.(value, prev)), [value]) + + return ( +
+
+ {title ? ( + {label} + ) : ( + {label} + )} + {unit} +
+
+ {val} + {isValidElement(icon) ? icon : icon?.value} +
+
+ ) +}) + +const iconRenderer = (value, prev) => { + if (!Number.isFinite(+value)) return null + if (prev?.prevDate + 1000 >= Date.now()) return prev + const val = +value + let Component = CaretRightOutlined + if ((prev?.prev ?? null) && val !== prev.prev) + Component = val > prev.prev ? CaretUpOutlined : CaretDownOutlined + + return { + prev: val, + prevDate: Date.now(), + value: , + } +} + +const modeNames = { + 0: 'Ручной', + 1: 'Бурение в роторе', + 2: 'Проработка', + 3: 'Бурение в слайде', + 4: 'Спуск СПО', + 5: 'Подъем СПО', + 6: 'Подъем с проработкой', + + 10: 'БЛОКИРОВКА', +} + +const params = [ + { label: 'Режим', accessorName: 'mode', format: (value) => modeNames[value] || '---' }, + { label: 'Пользователь', accessorName: 'user', title: 'Пользователь панели оператора' }, + { label: 'Рот.', unit: 'об/мин', accessorName: 'rotorSpeed', iconRenderer }, + { label: 'Долото', unit: 'м', accessorName: 'bitDepth', iconRenderer, format: 2 }, + { label: 'Забой', unit: 'м', accessorName: 'wellDepth', iconRenderer, format: 2 }, + { label: 'Расход', unit: 'л/ч', accessorName: 'flow', iconRenderer }, + { label: 'Расход х.х.', unit: 'л/ч', accessorName: 'flowIdle', iconRenderer }, + { label: 'MSE', unit: '%', accessorName: 'mse', format: 2 }, +] + +export const TelemetrySummary = memo(({ data }) => { + return ( +
+ {params.map((param, i) => ( + + ))} +
+ ) +}) + +export default TelemetrySummary diff --git a/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx b/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx deleted file mode 100644 index f25217c..0000000 --- a/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Tooltip } from 'antd' -import { Display } from '@components/Display' - -export const UserOfWell = ({ data }) => ( - Пользователь} - value={data[data.length - 1]?.user} - /> -) - -export default UserOfWell diff --git a/src/pages/Well/Telemetry/TelemetryView/archive_methods.js b/src/pages/Well/Telemetry/TelemetryView/archive_methods.js new file mode 100644 index 0000000..2e488d1 --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/archive_methods.js @@ -0,0 +1,102 @@ +import { range } from 'd3' + +export const DATA_COUNT = 2048 // Колличество точек на подгрузку графика +export const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков +export const LOADING_TRIGGER = 0.5 // Кол-во экранов до подгрузки + +export const getLoadingInterval = (loaded, endDate, interval) => { + // Если данные загружены и дата не заходит за тригер дозагрузка не требуется + if ( + (loaded && (loaded.start ?? null) !== null && (loaded.end ?? null) !== null) && + +endDate - interval * (LOADING_TRIGGER + 1) > loaded.start && + +endDate + interval * LOADING_TRIGGER < loaded.end + ) + return { loadingStartDate: endDate, newLoaded: loaded, loadingInterval: 0 } + + let loadingStartDate = +endDate - interval * (ADDITIVE_PAGES + 1) + let loadingEndDate = +endDate + interval * ADDITIVE_PAGES + + const newLoaded = { + start: loadingStartDate, + end: loadingEndDate + } + + if (loadingStartDate <= loaded?.end && loadingEndDate >= loaded.start) { + if (loadingStartDate >= loaded.start) loadingStartDate = loaded.end + if (loadingEndDate <= loaded.end) loadingEndDate = loaded.start + newLoaded.start = Math.min(loaded.start, loadingStartDate) + newLoaded.end = Math.max(loaded.end, loadingEndDate) + } + + const loadingInterval = Math.trunc((loadingEndDate - loadingStartDate) / 1000) + + return { + loadingStartDate: new Date(loadingStartDate), + newLoaded: { + start: new Date(newLoaded.start), + end: new Date(newLoaded.end), + }, + loadingInterval, + } +} + +const interpolationSearch = (data, begin, end, accessor) => { + const fy = (i) => new Date(data[i]?.[accessor] ?? 0) + const fx = (y, b, e) => Math.round(b + (y - fy(b)) * (e - b) / (fy(e) - fy(b))) + const findIdx = (val, startIdx, c) => { + let x = startIdx + let endIdx = data.length - 1 + if(val < fy(startIdx)) + return startIdx + if(val > fy(endIdx)) + return endIdx + for(let i = 0; i < c; i++){ + x = fx(val, startIdx, endIdx) + if(fy(x) < val) + startIdx = x + else + endIdx = x + if ((startIdx === endIdx)||(fy(startIdx) === fy(endIdx))) + return x + } + return x + } + let x0 = findIdx(begin, 0, 100) + let x1 = findIdx(end, x0, 100) + return { start: x0, end: x1, count: x1 - x0 } +} + +export const cutData = (data, beginDate, endDate) => { + if (data?.length > 0) { + let { start, end } = interpolationSearch(data, beginDate, endDate, 'date') + if (start > 0) start-- + if (end + 1 < end.length) end++ + return data.slice(start, end) + } + return data +} + +export const makeDateTimeDisabled = (dateLimit, chartInterval) => ({ + disabledDate: (date) => { + if (!date) return false + const dt = new Date(date).setHours(0, 0, 0, 0) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }, + disabledTime: (date) => ({ + disabledHours: () => range(24).filter(h => { + if (!date) return false + const dt = +new Date(date).setHours(h) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }), + disabledMinutes: () => range(60).filter(m => { + if (!date) return false + const dt = +new Date(date).setMinutes(m) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }), + disabledSeconds: () => range(60).filter(s => { + if (!date) return false + const dt = +new Date(date).setSeconds(s) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }) + }), +}) diff --git a/src/pages/Well/Telemetry/TelemetryView/cursorRender.jsx b/src/pages/Well/Telemetry/TelemetryView/cursorRender.jsx index ad96f61..30c914c 100644 --- a/src/pages/Well/Telemetry/TelemetryView/cursorRender.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/cursorRender.jsx @@ -1,38 +1,64 @@ import { Fragment } from 'react' +import { getByAccessor } from '@components/d3' import { Grid, GridItem } from '@components/Grid' import { getChartIcon, makeDisplayValue } from '@utils' +import moment from 'moment' -const defaultFormater = makeDisplayValue({ def: '---', fixed: 2 }) +const defaultFormatter = makeDisplayValue({ def: '---', fixed: 2 }) const defaultValueRender = (v, unit) => ( - <>{defaultFormater(v)} {unit ?? ''} + <>{defaultFormatter(v)} {unit ?? ''} ) -export const cursorRender = (group, data) => { +export const cursorRender = (group, data, flowData) => { const d = data.length > 0 ? data[0] : {} if (group.charts.length <= 0) return <> const firstChart = group.charts[0] const y = firstChart.y(d) + const yDate = moment(y) + const flow = flowData?.filter((row) => yDate.isBetween(row.dateStart, row.dateEnd, 's', '[]')) const yValue = firstChart.yAxis.format?.(y) ?? defaultValueRender(y, firstChart.yAxis.unit) const xFormat = (chart) => { const v = chart.x(d) return chart.xAxis.format?.(v) ?? defaultValueRender(v, chart.xAxis.unit) } + let j = 0 + return ( - - {yValue} - {group.charts.map((chart, i) => { - return ( + <> + + {yValue} + {group.charts.filter((chart) => chart.type !== 'rect_area').map((chart, i) => ( - {getChartIcon(chart)} + {getChartIcon(chart)} {chart.shortLabel || chart.label} {xFormat(chart)} - ) - })} - + ))} + + + {group.charts.filter((chart) => chart.type === 'rect_area').map((chart, i) => { + const minX = getByAccessor(chart.minXAccessor) + const maxX = getByAccessor(chart.maxXAccessor) + const value = (row) => {defaultFormatter(minX(row))} - {defaultFormatter(maxX(row))} + + return ( + + {getChartIcon(chart)} + {chart.shortLabel || chart.label} + {flow?.map((row, j) => ( + + {chart.xAxis.format?.(row) ?? value(row)} + + ))} + {chart.xAxis.unit} + + ) + })} + + ) } diff --git a/src/pages/Well/Telemetry/TelemetryView/dataset.js b/src/pages/Well/Telemetry/TelemetryView/dataset.js new file mode 100644 index 0000000..9ff5273 --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/dataset.js @@ -0,0 +1,104 @@ +import { formatDate } from '@utils' + +export const yAxis = { + type: 'time', + accessor: (d) => new Date(d.date), + format: (d) => formatDate(d, undefined, 'DD.MM.YYYY HH:mm:ss'), +} + +const dash = [7, 3] + +const makeDataset = (label, shortLabel, color, key, unit, other) => ({ + key, + label, + shortLabel, + color, + xAxis: { + type: 'linear', + accessor: key, + unit, + }, + type: 'line', + ...other, +}) + +const depthInBetween = (val, flow) => flow.depthStart <= val && val <= flow.depthEnd + +export const calcFlowData = (dataSaub, flowChart) => { + if (dataSaub.length < 2) return [] + let minDepth = Infinity, maxDepth = -Infinity + const dates = dataSaub.map((row) => { + const depth = row.wellDepth + if (depth < minDepth) minDepth = depth + if (depth > maxDepth) maxDepth = depth + return { date: new Date(row.date), depth: row.wellDepth } + }) + const out = flowChart.flatMap((flow) => { + if (flow.depthStart > maxDepth || flow.depthEnd < minDepth) return [] + const out = [] + let i = 0, j = 0 + while (i < dates.length) { + while (i < dates.length && !depthInBetween(dates[i].depth, flow)) i++ + j = i++ + while (i < dates.length && depthInBetween(dates[i].depth, flow)) i++ + if (j >= dates.length || j >= i) + break + out.push({ + ...flow, + dateStart: dates[j].date, + dateEnd: dates[i - 1].date, + }) + } + return out + }) + return out +} + +export const makeChartGroups = () => { + const makeAreaOptions = (accessor) => ({ + type: 'rect_area', + hideLabel: true, + minYAccessor: 'dateStart', + maxYAccessor: 'dateEnd', + minXAccessor: accessor + 'Min', + maxXAccessor: accessor + 'Max', + linkedTo: accessor, + }) + + return [ + [ + makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'), + makeDataset('Глубина скважины', 'Глубина скв','#7789A1', 'wellDepth', 'м', { dash }), + makeDataset('Расход', 'Расход','#007070', 'flow', 'л/с'), + makeDataset('Положение долота', 'Долото','#B39D59', 'bitPosition', 'м'), + makeDataset('Расход', 'Расход','#007070', 'flowMM', 'л/с', makeAreaOptions('flow')), + ], [ + makeDataset('Скорость блока', 'Скорость ТБ','#59B359', 'blockSpeed', 'м/ч'), + makeDataset('Скорость заданная', 'Скор зад-я','#95B359', 'blockSpeedSp', 'м/ч', { dash }), + ], [ + makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'), + makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'), + makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }), + makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }), + makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')), + ], [ + makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'), + makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }), + makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }), + makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')), + ], [ + makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'), + makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }), + makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }), + makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }), + makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'), + makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')), + ], [ + makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'), + makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }), + makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }), + makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }), + makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')), + ] + ] +} diff --git a/src/pages/Well/Telemetry/TelemetryView/index.jsx b/src/pages/Well/Telemetry/TelemetryView/index.jsx index 7a2ca50..7bd4e13 100644 --- a/src/pages/Well/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/index.jsx @@ -1,321 +1,326 @@ import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { BehaviorSubject, buffer, throttleTime } from 'rxjs' -import { Button, Select } from 'antd' +import { useSearchParams } from 'react-router-dom' +import { Alert, Button, Select } from 'antd' import { useWell } from '@asb/context' -import { makeDateSorter } from '@components/Table' -import { D3MonitoringCharts } from '@components/d3/monitoring' import LoaderPortal from '@components/LoaderPortal' -import { Grid, GridItem } from '@components/Grid' +import { CopyUrlButton } from '@components/CopyUrl' +import { D3MonitoringCharts } from '@components/d3/monitoring' import { invokeWebApiWrapperAsync } from '@components/factory' +import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' -import { formatDate, hasPermission, withPermissions } from '@utils' +import { formatDate, hasPermission, isRawDate, withPermissions } from '@utils' import { Subscribe } from '@services/signalr' import { - DrillFlowChartService, - OperationStatService, - TelemetryDataSaubService, - TelemetryDataSpinService + OperationStatService, + TelemetryDataSaubService, + TelemetryDataSpinService } from '@api' +import { makeChartGroups, yAxis } from './dataset' +import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods' import ActiveMessagesOnline from './ActiveMessagesOnline' +import TelemetrySummary from './TelemetrySummary' import WirelineRunOut from './WirelineRunOut' -import { CustomColumn } from './CustomColumn' -import { ModeDisplay } from './ModeDisplay' -import { UserOfWell } from './UserOfWells' -import cursorRender from './cursorRender' import { Setpoints } from './Setpoints' +import { cursorRender } from './cursorRender' import MomentStabPicEnabled from '@images/DempherOn.png' import MomentStabPicDisabled from '@images/DempherOff.png' import SpinPicEnabled from '@images/SpinEnabled.png' import SpinPicDisabled from '@images/SpinDisabled.png' -import '@styles/monitoring.less' -import '@styles/message.less' +import '@styles/pages/telemetry_view.less' +import '@styles/pages/message.less' const { Option } = Select -export const yAxis = { - type: 'time', - accessor: (d) => new Date(d.date), - format: (d) => formatDate(d, undefined, 'DD.MM.YYYY HH:mm:ss'), +const chartProps = { + yAxis, + chartName: 'monitoring', + yTicks: { visible: true, format: (d) => formatDate(d, 'YYYY-MM-DD') }, + plugins: { menu: { enabled: false }, cursor: { render: cursorRender } }, + style: { flexGrow: 1, height: 'auto', width: 'auto' }, } -const dash = [7, 3] - -const makeDataset = (label, shortLabel, color, key, unit, other) => ({ - key, - label, - shortLabel, - color, - xAxis: { - type: 'linear', - accessor: key, - unit, - }, - type: 'line', - ...other, -}) - -export const makeChartGroups = (flowChart) => { - const makeAreaOptions = (accessor) => ({ - type: 'rect_area', - data: flowChart, - hideLabel: true, - yAxis: { - type: 'linear', - accessor: 'depth', - }, - minXAccessor: 'depthStart', - maxXAccessor: 'depthEnd', - minYAccessor: accessor + 'Min', - maxYAccessor: accessor + 'Max', - linkedTo: accessor, - }) - - return [ - [ - makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'), - makeDataset('Глубина скважины', 'Глубина скв','#7789A1', 'wellDepth', 'м', { dash }), - makeDataset('Расход', 'Расход','#007070', 'flow', 'л/с'), - makeDataset('Положение долота', 'Долото','#B39D59', 'bitPosition', 'м'), - makeDataset('Расход', 'Расход','#007070', 'flowMM', 'л/с', makeAreaOptions('flow')), - ], [ - makeDataset('Скорость блока', 'Скорость ТБ','#59B359', 'blockSpeed', 'м/ч'), - makeDataset('Скорость заданная', 'Скор зад-я','#95B359', 'blockSpeedSp', 'м/ч', { dash }), - ], [ - makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'), - makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'), - makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }), - makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }), - makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')), - ], [ - makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'), - makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }), - makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }), - makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')), - ], [ - makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'), - makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }), - makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }), - makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }), - makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'), - makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')), - ], [ - makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'), - makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }), - makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }), - makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }), - makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')), - ] - ] -} - -const getLast = (data) => - Array.isArray(data) ? data.at(-1) : data - -const isMseEnabled = (dataSaub) => { - const lastData = getLast(dataSaub) - return (lastData?.mseState && 2) > 0 -} - -const isTorqueStabEnabled = (dataSpin) => { - const lastData = getLast(dataSpin) - return lastData?.state === 7 -} +const getLast = (data) => Array.isArray(data) ? data.at(-1) : data +const isMseEnabled = (dataSaub) => (getLast(dataSaub)?.mseState && 2) > 0 +const isTorqueStabEnabled = (dataSpin) => getLast(dataSpin)?.state === 7 const isSpinEnabled = (dataSpin) => { - const lastData = getLast(dataSpin) - return lastData?.state > 0 && lastData?.state !== 6 + const lastData = getLast(dataSpin) + return lastData?.state > 0 && lastData?.state !== 6 } export const normalizeData = (data) => data?.map(item => ({ - ...item, - rotorSpeed: item.rotorSpeed < 1 ? 0 : item.rotorSpeed, - rotorTorque: item.rotorTorque < 1 ? 0 : item.rotorTorque, - blockSpeed: Math.abs(item.blockSpeed) + ...item, + rotorSpeed: item.rotorSpeed < 1 ? 0 : item.rotorSpeed, + rotorTorque: item.rotorTorque < 1 ? 0 : item.rotorTorque, + blockSpeed: Math.abs(item.blockSpeed) })) ?? [] const dateSorter = makeDateSorter('date') +const defaultDate = () => new Date(Date.now() - defaultPeriod * 1000) + +const makeSubjectSubscription = (subject$, handler) => { + const subscription = subject$.pipe( + buffer(subject$.pipe(throttleTime(700))) + ).subscribe((data) => handler(data.flat().filter(Boolean))) + + return () => subscription.unsubscribe() +} + +const getRowDate = (row) => row && isRawDate(row.date) ? new Date(row.date) : null const TelemetryView = memo(() => { - const [dataSaub, setDataSaub] = useState([]) - const [dataSpin, setDataSpin] = useState([]) - const [chartInterval, setChartInterval] = useState(defaultPeriod) - const [showLoader, setShowLoader] = useState(false) - const [flowChartData, setFlowChartData] = useState([]) - const [rop, setRop] = useState(null) - const [domain, setDomain] = useState({}) - const [chartMethods, setChartMethods] = useState() + const [well, updateWell] = useWell() + const [searchParams, setSearchParams] = useSearchParams() - const [well, updateWell] = useWell() + const [dataSaub, setDataSaub] = useState([]) + const [dataSpin, setDataSpin] = useState([]) + const [showLoader, setShowLoader] = useState(false) + const [rop, setRop] = useState(null) + const [chartMethods, setChartMethods] = useState() - const saubSubject$ = useMemo(() => new BehaviorSubject(), []) - const spinSubject$ = useMemo(() => new BehaviorSubject(), []) + const [loadedDataRange, setLoadedDataRange] = useState(null) + const [chartInterval, setChartInterval] = useState(defaultPeriod * 1000) + const [endDate, setEndDate] = useState(defaultDate) + const [dateLimit, setDateLimit] = useState({ from: 0, to: Date.now() }) - const handleDataSaub = useCallback((data) => { - if (data) { - const dataSaub = normalizeData(data) - setDataSaub((prev) => { - const out = [...prev, ...dataSaub] - out.sort(dateSorter) - return out - }) - } - }, []) + const [archiveMode, setArchiveMode] = useState(false) - const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), []) + const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) - useEffect(() => { - const subscribtion = saubSubject$.pipe( - buffer(saubSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSaub(data.flat().filter(Boolean))) + const handleDataSaub = useCallback((data, replace = false) => { + setDataSaub((prev) => { + if (!data || !Array.isArray(data)) + return replace ? [] : prev + const dataSaub = normalizeData(data) + const out = replace ? [...dataSaub] : [...prev, ...dataSaub] + out.sort(dateSorter) + setLoadedDataRange({ + start: getRowDate(out.at(0)), + end: getRowDate(out.at(-1)), + }) + return out + }) + }, []) - return () => subscribtion.unsubscribe() - }, [saubSubject$]) + const handleDataSpin = useCallback((data, replace) => { + setDataSpin((prev) => { + if (!data || !Array.isArray(data)) + return replace ? [] : prev + return replace ? data : [...prev, ...data] + }) + }, []) - useEffect(() => { - const subscribtion = spinSubject$.pipe( - buffer(spinSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSpin(data.flat().filter(Boolean))) + const onWheel = useCallback((e) => { + if (!archiveMode && e.deltaY < 0) { + setArchiveMode(true) + } else if (archiveMode) { + setEndDate((prevEndDate) => { + const offset = e.deltaY / 100 * chartInterval * 0.15 // сдвиг в 15% интервала + const nextEndDate = +prevEndDate + offset + const firstPossibleDate = Math.max(loadedDataRange?.start || 0, dateLimit.from) + chartInterval + const lastPossibleDate = Math.min(dateLimit.to, (loadedDataRange?.end ?? Date.now())) + const out = new Date(Math.max(firstPossibleDate, Math.min(nextEndDate, lastPossibleDate))) + if (e.deltaY > 0 && +out >= dateLimit.to) { // Автопереход к актуальным данным при прокручивании в самый низ + setArchiveMode(false) + } + return out + }) + } + }, [archiveMode, loadedDataRange, chartInterval, dateLimit]) - return () => subscribtion.unsubscribe() - }, [spinSubject$]) + const dateTimeDisabled = useMemo(() => makeDateTimeDisabled(dateLimit, chartInterval), [dateLimit, chartInterval]) - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const flowChart = await DrillFlowChartService.getByIdWell(well.id) - const dataSaub = await TelemetryDataSaubService.getData(well.id, null, chartInterval) - const dataSpin = await TelemetryDataSpinService.getData(well.id, null, chartInterval) - setFlowChartData(flowChart ?? []) - handleDataSaub(dataSaub) - handleDataSpin(dataSpin) - }, - null, - `Не удалось получить данные`, - { actionName: 'Получение данных по скважине', well } - ) - }, [well, chartInterval, handleDataSpin, handleDataSaub]) + const domain = useMemo(() => ({ min: new Date(+endDate - chartInterval), max: endDate }), [endDate, chartInterval]) - useEffect(() => { - const unsubscribe = Subscribe( - 'hubs/telemetry', `well_${well.id}`, - { methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) }, - { methodName: 'ReceiveDataSpin', handler: (data) => spinSubject$.next(data) } - ) + const spinLast = useMemo(() => getLast(dataSpin), [dataSpin]) + const saubLast = useMemo(() => getLast(dataSaub), [dataSaub]) + const summaryData = useMemo(() => ({ ...saubLast, ...rop }), [saubLast, rop]) - return () => unsubscribe() - }, [well.id, saubSubject$, spinSubject$]) + const saubSubject$ = useMemo(() => new BehaviorSubject(), []) + const spinSubject$ = useMemo(() => new BehaviorSubject(), []) - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) - setRop(rop) - }, - setShowLoader, - `Не удалось загрузить данные`, - { actionName: 'Получение данных по скважине', well } - ) - }, [well]) + const filteredData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain]) + const chartGroups = useMemo(() => makeChartGroups(), []) - const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) + useEffect(() => { + if (!searchParams.has('range') || !searchParams.has('end')) return + setArchiveMode(isRawDate(searchParams.get('end'))) + const interval = parseInt(searchParams.get('range') || defaultPeriod) * 1000 + const date = new Date(searchParams.get('end') || Date.now()) + setChartInterval(interval) + setEndDate(date) + }, []) // Получение параметров должно работать только при открытии страницы - useEffect(() => { - if (dataSaub.length <= 0) return - const last = new Date(dataSaub.at(-1).date) - setDomain({ - min: new Date(+last - chartInterval * 1000), - max: last - }) - }, [dataSaub, chartInterval]) + useEffect(() => { + const params = {} + if (archiveMode) { + if (endDate) + params.end = endDate.toISOString() + if (chartInterval) + params.range = parseInt(chartInterval / 1000) + } + setSearchParams(params) + }, [archiveMode, endDate, chartInterval]) - const filteredData = useMemo(() => { - let i, j - for (i = 0; i < dataSaub.length; i++) { - const date = +new Date(dataSaub[i]?.date) - if (date >= +domain.min) break - } + useEffect(() => makeSubjectSubscription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub]) + useEffect(() => makeSubjectSubscription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) - for (j = dataSaub.length - 1; j >= i; j--) { - const date = +new Date(dataSaub[i]?.date) - if (date <= +domain.max) break - } + useEffect(() => { + if (archiveMode) return + const unsubscribe = Subscribe( + 'hubs/telemetry', `well_${well.id}`, + { methodName: 'ReceiveDataSaub', handler: (data) => saubSubject$.next(data) }, + { methodName: 'ReceiveDataSpin', handler: (data) => spinSubject$.next(data) } + ) - if (i >= j) return [] - return dataSaub.slice(i, j) - }, [dataSaub, domain]) + return () => unsubscribe() + }, [archiveMode, well.id, saubSubject$, spinSubject$]) - const chartGroups = useMemo(() => makeChartGroups(flowChartData), [flowChartData]) + useEffect(() => { + if (archiveMode) return + invokeWebApiWrapperAsync( + async () => { + const dataSaub = await TelemetryDataSaubService.getData(well.id, null, chartInterval / 1000) + const dataSpin = await TelemetryDataSpinService.getData(well.id, null, chartInterval / 1000) + handleDataSaub(dataSaub, true) + handleDataSpin(dataSpin, true) + }, + setShowLoader, + `Не удалось получить данные`, + { actionName: 'Получение данных по скважине', well } + ) + }, [archiveMode, chartInterval, well]) - return ( - - - -
- -
- Интервал:  - + useEffect(() => { + if (!archiveMode || showLoader) return + const { loadingStartDate, loadingInterval, newLoaded } = getLoadingInterval(loadedDataRange, endDate, chartInterval) + if (loadingInterval <= 0) return + invokeWebApiWrapperAsync( + async () => { + const data = await TelemetryDataSaubService.getData(well.id, loadingStartDate.toISOString(), loadingInterval, DATA_COUNT) + + const loadedStartDate = new Date(Math.max(+newLoaded.start, +endDate - chartInterval * (ADDITIVE_PAGES + 1))) + const loadedEndDate = new Date(Math.min(+newLoaded.end, +endDate + chartInterval * ADDITIVE_PAGES)) + setLoadedDataRange({ start: loadedStartDate, end: loadedEndDate }) + + if (data) { + data.forEach(elm => elm.date = new Date(elm.date)) + setDataSaub((prevDataSaub) => { + let newData = normalizeData(data) + if (+loadingStartDate > +loadedDataRange.start || (+loadingStartDate + loadingInterval * 1000) < +loadedDataRange.end) { + newData = [...prevDataSaub, ...newData] + } + newData.sort(dateSorter) + return cutData(newData, loadedStartDate, loadedEndDate) + }) + } + + }, + setShowLoader, + `Не удалось загрузить данные c ${formatDate(newLoaded.start)} по ${formatDate(newLoaded.end)}`, + { actionName: 'Загрузка телеметрий в диапозоне', well } + ) + }, [archiveMode, showLoader, well, chartInterval, loadedDataRange, endDate, handleDataSaub]) + + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) + setRop(rop) + let dates = await TelemetryDataSaubService.getDataDatesRange(well.id) + dates = { + from: new Date(dates?.from ?? 0), + to: new Date(dates?.to ?? 0) + } + setDateLimit(dates) + }, + setShowLoader, + e => `Не удалось загрузить данные: ${String(e)}`, + { actionName: 'Получение данных по скважине', well } + ) + }, [well]) + + useEffect(() => { + if (archiveMode || !saubLast || !isRawDate(saubLast.date)) return + setEndDate(new Date(saubLast.date)) + }, [archiveMode, saubLast]) + + return ( + +
+
+ +
+
+ Статус:  + +
+ + +
+ {'TorqueMaster'} + {'SpinMaster'} +

MSE

+
+
+ {archiveMode && ( +
+ +
+ )} +
+
+
+ Последняя дата:  + setEndDate(new Date(endDate))} + /> +
+
+ Интервал:  + setChartInterval(value * 1000)} /> +
+ + + {archiveMode && } +
+ +
- -
- Статус:  - -
- -   - -
- {'TorqueMaster'} - {'SpinMaster'} -

MSE

-
- -
- - - - - - formatDate(d, 'YYYY-MM-DD') - }} - plugins={{ - menu: { enabled: false }, - cursor: { - render: cursorRender, - }, - }} - height={'70vh'} - /> - - - - - - - ) + + ) }) export default withPermissions(TelemetryView, [ - 'DrillFlowChart.get', - 'OperationStat.get', - 'TelemetryDataSaub.get', - 'TelemetryDataSpin.get', - 'Well.get', + 'DrillFlowChart.get', + 'OperationStat.get', + 'TelemetryDataSaub.get', + 'TelemetryDataSpin.get', + 'Well.get', ]) diff --git a/src/pages/Well/WellCase/HistoryTable.jsx b/src/pages/Well/WellCase/HistoryTable.jsx index dd9ce30..6b7d41e 100644 --- a/src/pages/Well/WellCase/HistoryTable.jsx +++ b/src/pages/Well/WellCase/HistoryTable.jsx @@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import { WellFinalDocumentsService } from '@api' import { formatDate } from '@utils' -import '@styles/well_case.less' +import '@styles/pages/well_case.less' export const HistoryTable = memo(({ category }) => { const [isLoading, setIsLoading] = useState(false) @@ -23,7 +23,7 @@ export const HistoryTable = memo(({ category }) => { async () => { const result = await WellFinalDocumentsService.getFilesHistoryByIdCategory(well.id, category.idCategory) if (!result) return - const files = result.file + const files = result.files files.sort((a, b) => moment(a.uploadDate) - moment(b.uploadDate)) const fileSource = files.map((file) => ({ file, diff --git a/src/pages/Well/WellCase/WellCaseEditor.jsx b/src/pages/Well/WellCase/WellCaseEditor.jsx index dcc287c..1a61acc 100644 --- a/src/pages/Well/WellCase/WellCaseEditor.jsx +++ b/src/pages/Well/WellCase/WellCaseEditor.jsx @@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import { WellFinalDocumentsService } from '@api' import { arrayOrDefault } from '@utils' -import '@styles/well_case.less' +import '@styles/pages/well_case.less' const filterCategoriesByText = (text) => (cat) => !text || !!cat?.nameCategory?.toLowerCase().includes(text.toLowerCase()) diff --git a/src/pages/Well/WellCase/index.jsx b/src/pages/Well/WellCase/index.jsx index 3912da6..44d2a20 100644 --- a/src/pages/Well/WellCase/index.jsx +++ b/src/pages/Well/WellCase/index.jsx @@ -1,5 +1,6 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' -import { Alert, Button, Typography } from 'antd' +import { NotificationOutlined } from '@ant-design/icons' +import { Alert, Button, Popconfirm } from 'antd' import { useWell } from '@asb/context' import { UserView } from '@components/views' @@ -14,7 +15,7 @@ import { withPermissions } from '@utils' import WellCaseEditor from './WellCaseEditor' import { HistoryTable } from './HistoryTable' -import '@styles/well_case.less' +import '@styles/pages/well_case.less' const expandable = { expandedRowRender: (category) => , @@ -42,6 +43,17 @@ const WellCase = memo(() => { ) }, [well]) + const notifyPublisher = useCallback((category) => { + invokeWebApiWrapperAsync( + async () => { + await fetch(`/api/WellFinalDocuments/${well.id}/reNotifyPublishers?idCategory=${category.idCategory}`) + }, + setIsLoading, + `Не удалось повторно оповестить ответственного по "${category.nameCategory}"`, + { actionName: `Повторное оповещение ответственного по "${category.nameCategory}"`, well }, + ) + }, [well]) + const columns = useMemo(() => [ makeTextColumn('Категория', 'nameCategory', undefined, undefined, undefined, { width: 300 }), makeColumn('Файл', 'file', { @@ -49,14 +61,25 @@ const WellCase = memo(() => {
{file ? : Файл не загружен} - {category.permissionToUpload && ( - setIsLoading(true)} - onUploadComplete={updateTable} - onUploadError={() => setIsLoading(false)} - /> - )} +
+ {!file && canEdit && ( + notifyPublisher(category)} + title={'Повторно оповестить ответственного о необходимости подгрузки документа'} + > + + + )} + + {category.permissionToUpload && ( + setIsLoading(true)} + onUploadComplete={updateTable} + onUploadError={() => setIsLoading(false)} + /> + )} +
), width: 300, @@ -66,7 +89,7 @@ const WellCase = memo(() => { render: (publishers) => publishers?.map((user, i) => ), width: 200, }), - ], [well, updateTable]) + ], [well, canEdit, updateTable]) const onEditClose = useCallback((changed = false) => { setShowEdit(false) diff --git a/src/pages/Well/NavigationMenu.jsx b/src/pages/Well/WellNavigationMenu.jsx similarity index 90% rename from src/pages/Well/NavigationMenu.jsx rename to src/pages/Well/WellNavigationMenu.jsx index e6b9181..60df78f 100644 --- a/src/pages/Well/NavigationMenu.jsx +++ b/src/pages/Well/WellNavigationMenu.jsx @@ -4,7 +4,6 @@ import { BarChartOutlined, BuildOutlined, ControlOutlined, - DatabaseOutlined, DeploymentUnitOutlined, ExperimentOutlined, FilePdfOutlined, @@ -14,13 +13,12 @@ import { TableOutlined, } from '@ant-design/icons' -import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' +import { makeItem, PrivateMenu } from '@components/PrivateMenu' export const menuItems = [ makeItem('Телеметрия', 'telemetry', [], , [ makeItem('Мониторинг', 'telemetry', [], ), makeItem('Сообщения', 'messages', [], ), - makeItem('Архив', 'archive', [], ), makeItem('ННБ', 'dashboard_nnb', [], ), makeItem('Операции', 'operations', [], ), makeItem('Наработка', 'operation_time', [], ), @@ -42,7 +40,6 @@ export const menuItems = [ makeItem('План', 'plan', [], ), makeItem('Факт', 'fact', [], ), makeItem('РТК', 'drillProcessFlow', [], ), - makeItem('Режимы', 'params', [], ), ]), makeItem('Документы', 'document', [], , [ makeItem('Растворный сервис', 'fluidService', [], ), @@ -61,15 +58,15 @@ export const menuItems = [ makeItem('Дело скважины', 'well_case', [], ), ] -export const NavigationMenu = memo((props) => ( - ( + )) -export default NavigationMenu +export default WellNavigationMenu diff --git a/src/pages/Well/WellOperations/DrillProcessFlow.jsx b/src/pages/Well/WellOperations/DrillProcessFlow.jsx index 86dcb09..5396ebd 100644 --- a/src/pages/Well/WellOperations/DrillProcessFlow.jsx +++ b/src/pages/Well/WellOperations/DrillProcessFlow.jsx @@ -1,44 +1,110 @@ -import { useState, useEffect, memo, useMemo, useCallback } from 'react' +import { useState, useEffect, memo, useMemo, useCallback, FC } from 'react' +import { Button, Tooltip } from 'antd' +import { FileOutlined } from '@ant-design/icons' -import { useWell } from '@asb/context' +import { useWell, useTopRightBlock } from '@asb/context' +import { + EditableTable, + makeGroupColumn, + makeNumericColumn, + makeNumericColumnPlanFact, + makeNumericRender, + makeNumericSorter, + makeSelectColumn, +} from '@components/Table' import LoaderPortal from '@components/LoaderPortal' -import { invokeWebApiWrapperAsync } from '@components/factory' -import { EditableTable, makeNumericMinMax, makeNumericStartEnd } from '@components/Table' -import { DrillFlowChartService } from '@api' +import { invokeWebApiWrapperAsync, download } from '@components/factory' +import { ProcessMapService, WellOperationService } from '@api' import { arrayOrDefault } from '@utils' -const columns = [ - makeNumericStartEnd('Глубина, м', 'depth'), - makeNumericMinMax('Нагрузка, т', 'axialLoad'), - makeNumericMinMax('Давление, атм', 'pressure'), - makeNumericMinMax('Момент на ВСП, кН·м', 'rotorTorque'), - makeNumericMinMax('Обороты на ВСП, об/мин', 'rotorSpeed'), - makeNumericMinMax('Расход, л/с', 'flow') -] +const style = { margin: 4 } + +const ImportExportBar = memo(({ well: givenWell, disabled }) => { + const [wellContext] = useWell() + const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext]) + + const downloadExport = useCallback( + async () => await download(`/api/ProcessMap/getReportFile/${well.id}`), + [well.id] + ) + + return ( +
+ +
+ ) +}) + +const numericRender = makeNumericRender(2) + +export const getColumns = async (idWell) => { + let sectionTypes = await WellOperationService.getSectionTypes(idWell) + sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({ + label: value, + value: id, + })) + + return [ + makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, null, { + width: 160, + sorter: makeNumericSorter('idWellSectionType'), + }), + makeGroupColumn('Интервал бурения, м', [ + makeNumericColumn('От', 'depthStart', numericRender), + makeNumericColumn('До', 'depthEnd', numericRender), + ]), + makeNumericColumnPlanFact('Перепад давления, атм', 'pressure', numericRender), + makeNumericColumnPlanFact('Нагрузка, т', 'axialLoad', numericRender), + makeNumericColumnPlanFact('Момент на ВСП, кН·м', 'topDriveTorque', numericRender), + makeNumericColumnPlanFact('Обороты на ВСП, об/мин', 'topDriveSpeed', numericRender), + makeNumericColumnPlanFact('Расход, л/с', 'flow', numericRender), + makeNumericColumn('Плановая механическая скорость, м/ч', 'ropPlan', numericRender), + ] +} export const DrillProcessFlow = memo(() => { const [flows, setFlows] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [columns, setColumns] = useState([]) const [well] = useWell() + const setTopRightBlock = useTopRightBlock() const updateFlows = useCallback(() => invokeWebApiWrapperAsync( async () => { - const flows = await DrillFlowChartService.getByIdWell(well.id) + const flows = await ProcessMapService.getByIdWell(well.id) setFlows(arrayOrDefault(flows)) }, setShowLoader, `Не удалось загрузить режимно-технологическую карту`, - { actionName: 'Получение режимно-технологической карты', well } + { actionName: 'Получение режимно-технологической карты', well }, ), [well]) + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + const columns = await getColumns(well.id) + setColumns(columns) + }, + setShowLoader, + `Не удалось загрузить список конструкций секций`, + { actionName: 'Получение списка конструкций секций', well }, + ) + }, [well]) + useEffect(() => { updateFlows() }, [well]) + useEffect(() => setTopRightBlock((well) => ( + + )), [setTopRightBlock]) + const tableHandlers = useMemo(() => { const handlerProps = { - service: DrillFlowChartService, + service: ProcessMapService, setLoader: setShowLoader, onComplete: updateFlows, permission: 'DrillFlowChart.edit', @@ -49,7 +115,12 @@ export const DrillProcessFlow = memo(() => { return { add: { ...handlerProps, action: 'insert', actionName: 'Добавление месторождения', recordParser }, edit: { ...handlerProps, action: 'update', actionName: 'Редактирование месторождения', recordParser }, - delete: { ...handlerProps, action: 'delete', actionName: 'Удаление месторождения', permission: 'DrillFlowChart.delete' }, + delete: { + ...handlerProps, + action: 'delete', + actionName: 'Удаление месторождения', + permission: 'DrillFlowChart.delete', + }, } }, [updateFlows, well.id]) diff --git a/src/pages/Well/WellOperations/OperationEditor/WellOperationsEditor.jsx b/src/pages/Well/WellOperations/OperationEditor/WellOperationsEditor.jsx index 653ec16..08d548d 100644 --- a/src/pages/Well/WellOperations/OperationEditor/WellOperationsEditor.jsx +++ b/src/pages/Well/WellOperations/OperationEditor/WellOperationsEditor.jsx @@ -31,26 +31,20 @@ const dayWithoutNptRender = (_, row) => dayRender((row.day ?? 0) - (row.nptHours const generateColumns = (showNpt = false, categories = [], sectionTypes = []) => [ makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { sorter: makeNumericSorter('idWellSectionType'), - editable: true, width: 160, }), makeSelectColumn('Операция', 'idCategory', categories, undefined, { sorter: makeNumericSorter('idCategory'), - editable: true, width: 200, }), - makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null, { editable: true, width: 300, input: