Merge branch 'dev' into feature/limit-parameter-staistics-window

This commit is contained in:
goodmice 2022-12-22 10:38:59 +05:00
commit 860d74f2db
No known key found for this signature in database
GPG Key ID: 63EA771203189CF1
149 changed files with 1699 additions and 1018 deletions

15
.vscode/settings.json vendored
View File

@ -1,6 +1,17 @@
{ {
"cSpell.words": [ "cSpell.words": [
"день" "день",
"спиннера",
"Saub",
"КНБК",
"САУБ",
"antd",
"Poprompt",
"saub",
"setpoint",
"Setpoints",
"usehooks"
], ],
"liveServer.settings.port": 5501 "liveServer.settings.port": 5501,
"cSpell.language": "en,ru"
} }

170
CODE_STANDART.md Normal file
View File

@ -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 }) => (
<LoaderPortal show={loading}>
<div className={'dd-test-page'}>
<div className={'dd-test-page-title'}>{title}</div>
<div className={'dd-test-page-content'}>{content}</div>
</div>
</LoaderPortal>
))
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<TestPageProps>(({ title, content, loading }) => (
<LoaderPortal show={loading}>
<div className={'dd-test-page'}>
<div className={'dd-test-page-title'}>{title}</div>
<div className={'dd-test-page-content'}>{content}</div>
</div>
</LoaderPortal>
))
export default TestPage
```
3. Использование `any` в типах допустимо только, если значение используется только в параметрах компонентов, обозначенных типом `any`. Если метод предполагает работу с разными типами значений стоит описать его как обобщённый.
## 4. JSX/TSX
### 4.1. Стилизация кода
1. Все указываемые к компоненту параметры должны быть обёрнуты в фигурные скобки, кроме параметров флагов со значением `true`:
```jsx
<Button disabled title={'Hello, world!'} type={'ghost'}>Click me!</Button>
```
2. Если описание параметров компонента не укладывается в ширину в 120 строк стоит перенести их в соответствии с шаблоном:
```jsx
<Button
disabled
title={'Hello, world!'}
type={'ghost'}
>
Click me!
</Button>
```
3. Если JSX код передаётся как значение стоит обернуть его в круглые скобки:
```jsx
const a = (
<Button disabled title={'Hello, world!'} type={'ghost'}>Click me!</Button>
)
```
### 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` - для компонентов, применяющихся как виджеты в дашбордах.

View File

@ -7,7 +7,7 @@
Установка выполняется одной командой: Установка выполняется одной командой:
```bash ```bash
npm i npm ci
``` ```
## 2. Автогенерация сервисов ## 2. Автогенерация сервисов
@ -19,12 +19,12 @@ npm i
Если сервер запущен на текущей машине достаточно написать: Если сервер запущен на текущей машине достаточно написать:
```bash ```bash
npm run update_openapi npm run oul
``` ```
Для получения сервисов с основного сервера: Для получения сервисов с основного сервера:
```bash ```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-адреса:
| IP-адрес | Описание | | IP-адрес | Команда | Описание |
|:-|:-| |:-------------------------|:--------|:------------------------------------|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) | | 127.0.0.1:5000 | oul | Локальный адрес вашей машины |
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) | | 192.168.1.113:5000 | oud | Локальный адрес development-сервера |
| 46.146.209.148:89 | Внешний адрес development-сервера | | 46.146.209.148:89 | oug_dev | Внешний адрес development-сервера |
| cloud.digitaldrilling.ru | Внешний адрес production-сервера | | cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера |
## 3. Компиляция production-версии приложения ## 3. Компиляция production-версии приложения
После выполнения вышеописанных пунктов приложение готово к компиляции. После выполнения вышеописанных пунктов приложение готово к компиляции.

View File

@ -5,7 +5,7 @@ import { RootPathContext } from '@asb/context'
import SuspenseFallback from '@components/SuspenseFallback' import SuspenseFallback from '@components/SuspenseFallback'
import { NoAccessComponent } from '@utils' import { NoAccessComponent } from '@utils'
import '@styles/App.less' import '@styles/pages/App.less'
const UserOutlet = lazy(() => import('@components/outlets/UserOutlet')) const UserOutlet = lazy(() => import('@components/outlets/UserOutlet'))
const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet')) const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet'))

View File

@ -5,13 +5,13 @@ import { AutoComplete } from 'antd'
import { join } from 'path' import { join } from 'path'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { makeItem, PrivateWellMenuItem } from './PrivateWellMenu' import { makeItem, PrivateMenuItem } from './PrivateMenu'
import { hasPermission, isURLAvailable } from '@utils' import { hasPermission, isURLAvailable } from '@utils'
import { menuItems as adminMenuItems } from '@pages/AdminPanel/AdminNavigationMenu' import { menuItems as adminMenuItems } from '@pages/AdminPanel/AdminNavigationMenu'
import { menuItems as wellMenuItems } from '@pages/Well/NavigationMenu' import { menuItems as wellMenuItems } from '@pages/Well/WellNavigationMenu'
import '@styles/fast_run_menu.less' import '@styles/components/fast_run_menu.less'
const transliterationTable = { const transliterationTable = {
'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ', 'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ',
@ -29,7 +29,7 @@ const transliterateToEn = (text: string) => Object.entries(transliterationTable)
const applyVars = (route: string, vars?: object): string => !vars ? route : const applyVars = (route: string, vars?: object): string => !vars ? route :
Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route) Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route)
const makeOptions = (items: PrivateWellMenuItem[], vars?: object): OptionType[] => { const makeOptions = (items: PrivateMenuItem[], vars?: object): OptionType[] => {
const out: OptionType[] = [] const out: OptionType[] = []
items.forEach((item) => { items.forEach((item) => {
if (!hasPermission(item.permissions)) return if (!hasPermission(item.permissions)) return
@ -78,7 +78,7 @@ export const FastRunMenu = memo(() => {
] ]
if (isURLAvailable('/admin')) if (isURLAvailable('/admin'))
menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateWellMenuItem[])) menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateMenuItem[]))
if (well.id) if (well.id)
menus.push( menus.push(

View File

@ -1,7 +1,7 @@
import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd' import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd'
import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { ItemType } from 'antd/lib/menu/hooks/useItems' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, Outlet } from 'react-router-dom' import { Link, Outlet, useLocation } from 'react-router-dom'
import { import {
ApartmentOutlined, ApartmentOutlined,
CodeOutlined, CodeOutlined,
@ -18,7 +18,7 @@ import SuspenseFallback from './SuspenseFallback'
import Logo from '@images/Logo' import Logo from '@images/Logo'
import '@styles/layout.less' import '@styles/components/layout.less'
const { Content, Sider } = Layout const { Content, Sider } = Layout
@ -31,7 +31,7 @@ export type LayoutPortalProps = Omit<LayoutProps, 'children'> & {
siderProps?: SiderProps & { userMenuProps?: UserMenuProps } siderProps?: SiderProps & { userMenuProps?: UserMenuProps }
isAdmin?: boolean isAdmin?: boolean
fallback?: JSX.Element fallback?: JSX.Element
breadcrumb?: boolean | JSX.Element breadcrumb?: boolean | ((path: string) => JSX.Element)
topRightBlock?: JSX.Element topRightBlock?: JSX.Element
} }
@ -49,6 +49,7 @@ const _LayoutPortal = memo(() => {
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false) const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
const [currentWell, setCurrentWell] = useState<string>('') const [currentWell, setCurrentWell] = useState<string>('')
const [props, setProps] = useState<LayoutPortalProps>(defaultProps) const [props, setProps] = useState<LayoutPortalProps>(defaultProps)
const location = useLocation()
const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props]) const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props])
@ -66,6 +67,8 @@ const _LayoutPortal = memo(() => {
makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)), makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)),
].filter(Boolean) as ItemType[], [isAdmin, currentWell]) ].filter(Boolean) as ItemType[], [isAdmin, currentWell])
const breadcrumbItems = useMemo(() => typeof breadcrumb === 'function' && breadcrumb(location.pathname), [breadcrumb, location.pathname])
return ( return (
<Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}> <Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}>
{(sider || siderProps) && ( {(sider || siderProps) && (
@ -114,7 +117,7 @@ const _LayoutPortal = memo(() => {
<a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a> <a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a>
</Breadcrumb.Item> </Breadcrumb.Item>
)} )}
{breadcrumb !== true && breadcrumb} {breadcrumbItems}
</Breadcrumb> </Breadcrumb>
)} )}
{topRightBlock} {topRightBlock}

View File

@ -3,12 +3,19 @@ import { HTMLAttributes } from 'react'
import { Loader } from '@components/icons' import { Loader } from '@components/icons'
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & { type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
/** Показать ли загрузку */
show?: boolean, show?: boolean,
/** Затемнять ли дочерний блок */
fade?: boolean, fade?: boolean,
/** Параметры спиннера */
spinnerProps?: HTMLAttributes<HTMLDivElement>, spinnerProps?: HTMLAttributes<HTMLDivElement>,
/** Заполнять ли контент на 100% */
fillContent?: boolean fillContent?: boolean
} }
/**
* @description Добавляет оверлей загрузки над обёрнутым блоком
*/
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => ( export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => (
<div className={`loader-container ${className}`} {...other}> <div className={`loader-container ${className}`} {...other}>
<div className={`loader-content${fillContent ? ' loader-content-fill' : ''}`}>{children}</div> <div className={`loader-content${fillContent ? ' loader-content-fill' : ''}`}>{children}</div>

View File

@ -2,16 +2,16 @@ import { Breadcrumb, BreadcrumbItemProps } from 'antd'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { PrivateWellMenuItem } from '@components/PrivateWellMenu' import { PrivateMenuItem } from '@components/PrivateMenu'
import { FunctionalValue, getFunctionalValue, } from '@utils' import { FunctionalValue, getFunctionalValue, } from '@utils'
export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: string[], root: string = '/') => { export const makeBreadcrumbItems = (items: PrivateMenuItem[], pathParts: string[], root: string = '/') => {
const out = [] const out = []
const parts = [...pathParts] const parts = [...pathParts]
let route = root let route = root
let arr: PrivateWellMenuItem[] | undefined = items let arr: PrivateMenuItem[] | undefined = items
while (arr && parts.length > 0) { while (arr && parts.length > 0) {
const child: PrivateWellMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase()) const child: PrivateMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase())
if (!child) break if (!child) break
route = join(route, child.route) route = join(route, child.route)
out.push({ ...child, route }) out.push({ ...child, route })
@ -21,13 +21,12 @@ export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: str
return out return out
} }
export const makeMenuBreadcrumbItems = ( export const makeMenuBreadcrumbItemsRender = (
menuItems: PrivateWellMenuItem[], menuItems: PrivateMenuItem[],
path: string,
pathRoot: RegExp = /^\//, pathRoot: RegExp = /^\//,
itemsProps?: FunctionalValue<(item: PrivateWellMenuItem) => BreadcrumbItemProps>, itemsProps?: FunctionalValue<(item: PrivateMenuItem) => BreadcrumbItemProps>,
itemRender?: (item: PrivateWellMenuItem) => JSX.Element, itemRender?: (item: PrivateMenuItem) => JSX.Element,
) => { ) => (path: string) => {
const getItemProps = getFunctionalValue(itemsProps) const getItemProps = getFunctionalValue(itemsProps)
const rootPart = pathRoot.exec(path) const rootPart = pathRoot.exec(path)

View File

@ -1,21 +1,21 @@
import { ItemType } from 'antd/lib/menu/hooks/useItems' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Menu, MenuProps } from 'antd'
import { memo, ReactNode, useMemo } from 'react' import { memo, ReactNode, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { Menu, MenuProps } from 'antd'
import { hasPermission, Permission } from '@utils' import { hasPermission, Permission } from '@utils'
export type PrivateWellMenuItem = { export type PrivateMenuItem = {
title: string title: string
route: string route: string
permissions: Permission | Permission[] permissions: Permission | Permission[]
icon?: ReactNode icon?: ReactNode
visible?: boolean visible?: boolean
children?: PrivateWellMenuItem[] children?: PrivateMenuItem[]
} }
const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => { const makeItems = (items: PrivateMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
return items.map((item) => { return items.map((item) => {
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
@ -43,11 +43,11 @@ const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser
}).filter(Boolean) }).filter(Boolean)
} }
const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => { const makeItemList = (items: PrivateMenuItem[], rootPath: string, variables: Record<string, number | string>): ItemType[] => {
const parser = (path: string, parent: string) => { const parser = (path: string, parent: string) => {
if (!path.startsWith('/')) if (!path.startsWith('/'))
path = join(parent, path) path = join(parent, path)
return path.replace(/\{wellId\}/, String(wellId)) return Object.entries(variables).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, String(value)), path)
} }
return makeItems(items, rootPath, parser) return makeItems(items, rootPath, parser)
@ -58,9 +58,9 @@ export const makeItem = (
route: string, route: string,
permissions: Permission | Permission[], permissions: Permission | Permission[],
icon?: ReactNode, icon?: ReactNode,
children?: PrivateWellMenuItem[], children?: PrivateMenuItem[],
visible?: boolean visible?: boolean
): PrivateWellMenuItem => ({ ): PrivateMenuItem => ({
title, title,
route, route,
icon, icon,
@ -69,16 +69,16 @@ export const makeItem = (
visible, visible,
}) })
export type PrivateWellMenuProps = Omit<MenuProps, 'items'> & { export type PrivateMenuProps = Omit<MenuProps, 'items'> & {
idWell?: number variables?: Record<string, number | string>
items: PrivateWellMenuItem[] items: PrivateMenuItem[]
rootPath?: string rootPath?: string
} }
export const PrivateWellMenu = memo<PrivateWellMenuProps>(({ idWell, items, rootPath = '/', ...other }) => { export const PrivateMenu = memo<PrivateMenuProps>(({ variables, items, rootPath = '/', ...other }) => {
const location = useLocation() const location = useLocation()
const menuItems = useMemo(() => makeItemList(items, rootPath, idWell), [items, rootPath, idWell]) const menuItems = useMemo(() => makeItemList(items, rootPath, variables || {}), [items, rootPath, variables])
const tabKeys = useMemo(() => { const tabKeys = useMemo(() => {
const out = [] const out = []

View File

@ -5,6 +5,11 @@ import { DatePickerWrapper, getObjectByDeepKey } from '..'
import { DatePickerWrapperProps } from '../DatePickerWrapper' import { DatePickerWrapperProps } from '../DatePickerWrapper'
import { formatDate, isRawDate } from '@utils' import { formatDate, isRawDate } from '@utils'
/**
* Фабрика методов сортировки столбцов для данных типа **Дата**
* @param key Ключ столбца
* @returns Метод сортировки
*/
export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => { export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => {
const vA = a ? getObjectByDeepKey(key, a) : null const vA = a ? getObjectByDeepKey(key, a) : null
const vB = b ? getObjectByDeepKey(key, b) : null const vB = b ? getObjectByDeepKey(key, b) : null
@ -16,6 +21,17 @@ export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> =>
return (new Date(vA)).getTime() - (new Date(vB)).getTime() return (new Date(vA)).getTime() - (new Date(vB)).getTime()
} }
/**
* Фабрика объектов-столбцов для компонента `Table` для работы с данными типа **Дата**
*
* @param title Название столбца
* @param key Ключ столбца
* @param utc Конвертировать ли дату в UTC
* @param format Формат отображения даты
* @param other Дополнительные опции столбца
* @param pickerOther Опции компонента селектора даты
* @returns Объект-столбец для работы с данными типа **Дата**
*/
export const makeDateColumn = <T extends unknown>( export const makeDateColumn = <T extends unknown>(
title: ReactNode, title: ReactNode,
key: string, key: string,
@ -24,6 +40,7 @@ export const makeDateColumn = <T extends unknown>(
other?: ColumnProps<T>, other?: ColumnProps<T>,
pickerOther?: DatePickerWrapperProps, pickerOther?: DatePickerWrapperProps,
) => makeColumn<T>(title, key, { ) => makeColumn<T>(title, key, {
editable: true,
...other, ...other,
render: (date) => ( render: (date) => (
<div className={'text-align-r-container'}> <div className={'text-align-r-container'}>

View File

@ -9,7 +9,6 @@ import { OmitExtends } from '@utils/types'
export * from './date' export * from './date'
export * from './time' export * from './time'
export * from './numeric' export * from './numeric'
export * from './plan_fact'
export * from './select' export * from './select'
export * from './tag' export * from './tag'
export * from './text' export * from './text'
@ -45,6 +44,7 @@ export const makeColumn = <T = any>(title: ReactNode, key: Key, other?: ColumnPr
title: title, title: title,
key: key, key: key,
dataIndex: key, dataIndex: key,
render: (value: T) => value,
...other, ...other,
}) })

View File

@ -1,4 +1,3 @@
import { ColumnFilterItem } from 'antd/lib/table/interface'
import { InputNumber } from 'antd' import { InputNumber } from 'antd'
import { Key, ReactNode } from 'react' import { Key, ReactNode } from 'react'
@ -46,18 +45,17 @@ export const makeNumericColumnOptions = <T extends number>(fixed?: number, sorte
export const makeNumericColumn = <T extends number>( export const makeNumericColumn = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
other?: ColumnProps<T>, other?: ColumnProps<T>,
) => makeColumn(title, key, { ) => makeColumn(title, key, {
filters, editable: true,
onFilter: filterDelegate ? filterDelegate(key) : undefined, onFilter: filterDelegate ? filterDelegate(key) : undefined,
sorter: makeNumericSorter(key), sorter: makeNumericSorter(key),
width, width,
input: <InputNumber style={{ width: '100%' }}/>, input: <InputNumber style={{ width: '100%' }} defaultValue={0} />,
render: renderDelegate ?? makeNumericRender<T>(2), render: renderDelegate || makeNumericRender<T>(2),
align: 'right', align: 'right',
...other ...other
}) })
@ -65,54 +63,78 @@ export const makeNumericColumn = <T extends number>(
export const makeNumericColumnPlanFact = <T extends number>( export const makeNumericColumnPlanFact = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number,
other?: ColumnProps<T>,
) => {
return {
title,
children: [
makeNumericColumn<T>('План', `${key}.plan`, renderDelegate, filterDelegate, width, other),
makeNumericColumn<T>('Факт', `${key}.fact`, renderDelegate, filterDelegate, width, other),
]
}
}
/**
* @deprecated Для значений типа план/факт появилась модель `PlanFactDto`, использование 2 полей с суффиксами неактуально
* @param title Заголовок столбца
* @param key Ключ столбца
* @param filters Список значений для фильтрации
* @param filterDelegate Метод фильтрации
* @param renderDelegate Render-метод отображения ячейки
* @param width Ширина столбца
* @param other Дополнительные опции
* @returns Объект-столбец для таблицы
*/
export const makeNumericColumnPlanFactOld = <T extends number>(
title: ReactNode,
key: Key,
renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
other?: ColumnProps<T>, other?: ColumnProps<T>,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width, other), makeNumericColumn<T>('План', key + 'Plan', renderDelegate, filterDelegate, width, other),
makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width, other), makeNumericColumn<T>('Факт', key + 'Fact', renderDelegate, filterDelegate, width, other),
]) ])
export const makeNumericStartEnd = <T extends number>( export const makeNumericStartEnd = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')), makeNumericColumn<T>('старт', key + 'Start', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')),
makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'End')) makeNumericColumn<T>('конец', key + 'End', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'End'))
]) ])
export const makeNumericMinMax = <T extends number>( export const makeNumericMinMax = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')), makeNumericColumn<T>('мин', key + 'Min', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')),
makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')), makeNumericColumn<T>('макс', key + 'Max', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')),
]) ])
export const makeNumericAvgRange = <T extends number>( export const makeNumericAvgRange = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', `${key}.min`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)), makeNumericColumn<T>('мин', `${key}.min`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)),
makeNumericColumn<T>('сред', `${key}.avg`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)), makeNumericColumn<T>('сред', `${key}.avg`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)),
makeNumericColumn<T>('макс', `${key}.max`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)), makeNumericColumn<T>('макс', `${key}.max`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)),
]) ])
export default makeNumericColumn export default makeNumericColumn

View File

@ -1,22 +0,0 @@
import { Key, ReactNode } from 'react'
import { ColumnProps, makeColumn } from '.'
export const makeColumnsPlanFact = <T,>(
title: string | ReactNode,
key: Key | [Key, Key],
columsOther?: ColumnProps<T> | [ColumnProps<T>, ColumnProps<T>],
) => {
const keys = Array.isArray(key) ? key : [`${key}Plan`, `${key}Fact`]
const others = Array.isArray(columsOther) ? columsOther : [columsOther, columsOther]
return {
title,
children: [
makeColumn<T>('план', keys[0], others[0]),
makeColumn<T>('факт', keys[1], others[1]),
]
}
}
export default makeColumnsPlanFact

View File

@ -1,9 +1,17 @@
import { Select, SelectProps } from 'antd' import { Select, SelectProps } from 'antd'
import { DefaultOptionType, SelectValue } from 'antd/lib/select' import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { Key, ReactNode } from 'react' import { Key, ReactNode, useMemo } from 'react'
import { ColumnProps, makeColumn } from '.' import { ColumnProps, makeColumn } from '.'
const findOption = <T extends DefaultOptionType>(value: any, options: T[] | undefined) =>
options?.find((option) => String(option?.value) === String(value))
const SelectWrapper = ({ value, options, ...other }: SelectProps) => {
const selectValue = useMemo(() => findOption(value, options)?.label, [value, options])
return <Select value={selectValue} options={options} {...other} />
}
export const makeSelectColumn = <T extends DefaultOptionType>( export const makeSelectColumn = <T extends DefaultOptionType>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
@ -12,10 +20,11 @@ export const makeSelectColumn = <T extends DefaultOptionType>(
other?: ColumnProps<T>, other?: ColumnProps<T>,
selectOther?: SelectProps<SelectValue> selectOther?: SelectProps<SelectValue>
) => makeColumn(title, key, { ) => makeColumn(title, key, {
editable: true,
...other, ...other,
input: <Select options={options} {...selectOther}/>, input: <SelectWrapper options={options} {...selectOther}/>,
render: (value, dataset, index) => { render: (value, dataset, index) => {
const item = options?.find(option => String(option?.value) === String(value)) const item = findOption(value, options)
return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--' return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--'
} }
}) })

View File

@ -65,6 +65,7 @@ export const makeTagColumn = <T extends DataType>(
const InputComponent = makeTagInput<T>(value_key, label_key) const InputComponent = makeTagInput<T>(value_key, label_key)
return makeColumn(title, dataIndex, { return makeColumn(title, dataIndex, {
editable: true,
...other, ...other,
render: (item: T[] | undefined, dataset, index) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm, dataset, index) ?? elm[label_key]}</Tag>) ?? '-', render: (item: T[] | undefined, dataset, index) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm, dataset, index) ?? elm[label_key]}</Tag>) ?? '-',
input: <InputComponent {...tagOther} options={options} />, input: <InputComponent {...tagOther} options={options} />,

View File

@ -1,3 +1,4 @@
import { Tooltip } from 'antd'
import { ColumnFilterItem } from 'antd/lib/table/interface' import { ColumnFilterItem } from 'antd/lib/table/interface'
import { Key, ReactNode } from 'react' import { Key, ReactNode } from 'react'
@ -15,6 +16,18 @@ export const makeStringSorter = <T extends string>(key: Key): SorterMethod<T> =>
return String(vA).localeCompare(String(vB)) return String(vA).localeCompare(String(vB))
} }
export const makeTextRender = <T extends string>(def = '---', stringCutter?: (text: string) => string) => (value: T) => {
if (!value) return def
if (stringCutter) {
return (
<Tooltip title={value}>
{stringCutter(value)}
</Tooltip>
)
}
return value
}
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) => export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
(filterValue: T, dataItem: DataType<T>) => dataItem[key] === filterValue (filterValue: T, dataItem: DataType<T>) => dataItem[key] === filterValue
@ -26,10 +39,11 @@ export const makeTextColumn = <T extends unknown = any>(
render?: RenderMethod<T>, render?: RenderMethod<T>,
other?: ColumnProps other?: ColumnProps
) => makeColumn(title, key, { ) => makeColumn(title, key, {
editable: true,
filters, filters,
onFilter: filters ? makeFilterTextMatch(key) : undefined, onFilter: filters ? makeFilterTextMatch(key) : undefined,
sorter: sorter ?? makeStringSorter(key), sorter: sorter || makeStringSorter(key),
render: render, render: render || makeTextRender(),
...other ...other
}) })

View File

@ -24,6 +24,7 @@ export const makeTimeColumn = <T extends TimeDto>(
other?: ColumnProps, other?: ColumnProps,
pickerOther?: TimePickerWrapperProps, pickerOther?: TimePickerWrapperProps,
) => makeColumn<T>(title, key, { ) => makeColumn<T>(title, key, {
editable: true,
...other, ...other,
render: (time) => ( render: (time) => (
<div className={'text-align-r-container'}> <div className={'text-align-r-container'}>

View File

@ -6,8 +6,11 @@ import moment, { Moment } from 'moment'
import { defaultFormat } from '@utils' import { defaultFormat } from '@utils'
export type DatePickerWrapperProps = PickerDateProps<Moment> & { export type DatePickerWrapperProps = PickerDateProps<Moment> & {
/** Значение селектора */
value?: Moment, value?: Moment,
/** Метод вызывается при изменений даты */
onChange?: (date: Moment | null) => any onChange?: (date: Moment | null) => any
/** Конвертировать ли значение в UTC */
isUTC?: boolean isUTC?: boolean
} }

View File

@ -9,11 +9,21 @@ import { defaultFormat } from '@utils'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & { export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
value?: RangeValue<Moment>, /** Значение селектора в виде массива из 2 элементов (от, до) */
value?: RangeValue<Moment>
/** Конвертировать ли значения в UTC */
isUTC?: boolean isUTC?: boolean
/** Разрешить сброс значения селектора */
allowClear?: boolean allowClear?: boolean
} }
/**
* Подготавливает значения к передаче в селектор
*
* @param value Массиз из 2 дат
* @param isUTC Конвертировать ли значения в UTC
* @returns Подготовленные даты
*/
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => { const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
if (!value) return [null, null] if (!value) return [null, null]
return [ return [
@ -22,7 +32,7 @@ const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue
] ]
} }
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear = false, ...other }) => ( export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear, ...other }) => (
<RangePicker <RangePicker
showTime showTime
allowClear={allowClear} allowClear={allowClear}
@ -31,7 +41,7 @@ export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, all
moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day'),
moment().startOf('day'), moment().startOf('day'),
]} ]}
value={normalizeDates(value)} value={normalizeDates(value, isUTC)}
{...other} {...other}
/> />
)) ))

View File

@ -1,6 +1,6 @@
import { Key, memo, useCallback, useEffect, useState } from 'react' import { Key, memo, useCallback, useEffect, useState } from 'react'
import { ColumnGroupType, ColumnType } from 'antd/lib/table' import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { Table as RawTable, TableProps } from 'antd' import { Table as RawTable, TableProps as RawTableProps } from 'antd'
import { RenderMethod } from './Columns' import { RenderMethod } from './Columns'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -14,16 +14,28 @@ export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings> export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>
export type TableColumns<T> = TableColumn<T>[] export type TableColumns<T> = TableColumn<T>[]
export type TableContainer<T> = TableProps<T> & { export type TableProps<T> = RawTableProps<T> & {
/** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */
columns: TableColumn<T>[] columns: TableColumn<T>[]
/** Название таблицы для сохранения настроек */
tableName?: string tableName?: string
/** Отображать ли кнопку настроек */
showSettingsChanger?: boolean showSettingsChanger?: boolean
} }
export interface DataSet<T, D = any> { export interface DataSet<T, D = any> {
[k: Key]: DataSet<T> | T | D [k: Key]: DataSet<T, D> | T | D
} }
/**
* Получить значение из объекта по составному ключу
*
* Составной ключ имеет вид: `<поле 1>[.<поле 2>...]`
*
* @param key Составной ключ
* @param data Объект из которого будет полученно значение
* @returns Значение, найденное по ключу, либо `undefined`
*/
export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => { export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => {
if (!key) return undefined if (!key) return undefined
const parts = String(key).split('.') const parts = String(key).split('.')
@ -36,15 +48,24 @@ export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>):
return out as T return out as T
} }
/**
* Фабрика обёрток render-функций ячеек с поддержкой составных ключей
* @param key Составной ключ
* @param render Стандартная render-функция
* @returns Обёрнутая render-функция
*/
export const makeColumnRenderWrapper = <T extends DataSet<any>>(key: Key | undefined, render: RenderMethod<T, T> | undefined): RenderMethod<T, T> => export const makeColumnRenderWrapper = <T extends DataSet<any>>(key: Key | undefined, render: RenderMethod<T, T> | undefined): RenderMethod<T, T> =>
(_: any, dataset: T, index: number) => { (_: any, dataset: T, index: number) => {
const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record) const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record)
return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index) return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index)
} }
/**
const applyColumnWrappers = <T extends DataSet<any>>(columns: BaseTableColumn<T>[]): BaseTableColumn<T>[] => { * Применяет необходимые обёртки ко всем столбцам таблицы
return columns.map((column) => { * @param columns Исходные столбцы
* @returns Обёрнутые столбцы
*/
const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => {
if ('children' in column) { if ('children' in column) {
return { return {
...column, ...column,
@ -55,17 +76,16 @@ const applyColumnWrappers = <T extends DataSet<any>>(columns: BaseTableColumn<T>
...column, ...column,
render: makeColumnRenderWrapper<T>(column.key, column.render), render: makeColumnRenderWrapper<T>(column.key, column.render),
} }
}) })
}
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) { function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps<T>) {
const [newColumns, setNewColumns] = useState<TableColumn<T>[]>([]) const [newColumns, setNewColumns] = useState<TableColumn<T>[]>([])
const [settings, setSettings] = useState<TableSettings>({}) const [settings, setSettings] = useState<TableSettings>({})
const onSettingsChanged = useCallback((settings?: TableSettings | null) => { const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
if (tableName) if (tableName)
setTableSettings(tableName, settings) setTableSettings(tableName, settings)
setSettings(settings ?? {}) setSettings(settings || {})
}, [tableName]) }, [tableName])
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
@ -92,6 +112,13 @@ function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSe
) )
} }
/**
* Обёртка над компонентом таблицы AntD
*
* Особенности:
* * Поддержка составных ключей столбцов
* * Работа с настройками столбцов таблицы
*/
export const Table = memo(_Table) as typeof _Table export const Table = memo(_Table) as typeof _Table
export default Table export default Table

View File

@ -6,8 +6,11 @@ import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils'
import { TimeDto } from '@api' import { TimeDto } from '@api'
export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & { export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & {
/** Текущее значение */
value?: TimeDto, value?: TimeDto,
/** Метод вызывается при изменений времени */
onChange?: (date: TimeDto | null) => any onChange?: (date: TimeDto | null) => any
/** Конвертировать ли время в UTC */
isUTC?: boolean isUTC?: boolean
} }

View File

@ -17,6 +17,13 @@ export type PaginationContainer<T> = {
items?: T[] | null items?: T[] | null
} }
/**
* Генерирует объект пагинации для компонента `Table` из данных от сервисов
*
* @param сontainer данные от сервиса
* @param other Дополнительные поля (передаются в объект напрямую в приоритете)
* @returns Объект пагинации
*/
export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({ export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({
...other, ...other,
pageSize: сontainer.take, pageSize: сontainer.take,

View File

@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { isURLAvailable, removeUser } from '@utils' import { isURLAvailable, removeUser } from '@utils'
import { AuthService } from '@api' import { AuthService } from '@api'
import '@styles/user_menu.less' import '@styles/components/user_menu.less'
export type UserMenuProps = DrawerProps & { export type UserMenuProps = DrawerProps & {
isAdmin?: boolean isAdmin?: boolean

View File

@ -36,7 +36,7 @@ import type {
ChartTicks ChartTicks
} from './types' } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
const defaultOffsets: ChartOffset = { const defaultOffsets: ChartOffset = {
top: 10, top: 10,

View File

@ -4,10 +4,10 @@ import { Property } from 'csstype'
import * as d3 from 'd3' import * as d3 from 'd3'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { usePartialProps } from '@utils'
import { ChartOffset } from './types' import { ChartOffset } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
import { usePartialProps } from '@asb/utils'
export type PercentChartDataType = { export type PercentChartDataType = {
name: string name: string

View File

@ -3,7 +3,7 @@ import * as d3 from 'd3'
import { ChartOffset } from './types' import { ChartOffset } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3MouseState = { export type D3MouseState = {
/** Позиция мыши по оси X */ /** Позиция мыши по оси X */

View File

@ -9,7 +9,7 @@ import { getChartIcon, isDev, usePartialProps } from '@utils'
import { BaseDataType } from '../types' import { BaseDataType } from '../types'
import { ChartGroup, ChartSizes } from './D3MonitoringCharts' import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
import '@styles/d3.less' import '@styles/components/d3.less'
type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[], flowData: DataType[] | undefined) => ReactNode type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[], flowData: DataType[] | undefined) => ReactNode

View File

@ -6,7 +6,7 @@ import { usePartialProps } from '@utils'
import { wrapPlugin } from './base' import { wrapPlugin } from './base'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3CursorSettings = { export type D3CursorSettings = {
/** Параметры стиля линии */ /** Параметры стиля линии */

View File

@ -8,7 +8,7 @@ import { BaseDataType, ChartRegistry } from '@components/d3/types'
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone' import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
import { getTouchedElements, wrapPlugin } from './base' import { getTouchedElements, wrapPlugin } from './base'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none' export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'

View File

@ -1,7 +1,7 @@
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { DepositsContext } from '@asb/context' import { DepositListContext } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DepositDto, DepositService } from '@api' import { DepositDto, DepositService } from '@api'
@ -24,11 +24,11 @@ export const DepositsOutlet = memo(() => {
}, []) }, [])
return ( return (
<DepositsContext.Provider value={deposits}> <DepositListContext.Provider value={deposits}>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>
<Outlet /> <Outlet />
</LoaderPortal> </LoaderPortal>
</DepositsContext.Provider> </DepositListContext.Provider>
) )
}) })

View File

@ -1,7 +1,7 @@
import { Tag, TreeSelect } from 'antd' import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { useDeposits } from '@asb/context' import { useDepositList } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils' import { hasPermission } from '@utils'
@ -39,7 +39,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
const [wellsTree, setWellsTree] = useState([]) const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([]) const [wellLabels, setWellLabels] = useState([])
const deposits = useDeposits() const deposits = useDepositList()
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -59,6 +59,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
<TreeSelect <TreeSelect
multiple multiple
treeCheckable treeCheckable
maxTagCount={'responsive'}
showCheckedStrategy={TreeSelect.SHOW_CHILD} showCheckedStrategy={TreeSelect.SHOW_CHILD}
treeDefaultExpandAll treeDefaultExpandAll
treeData={wellsTree} treeData={wellsTree}

View File

@ -2,7 +2,7 @@ import { Drawer, Tree, TreeDataNode, TreeProps } from 'antd'
import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react' import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useDeposits } from '@asb/context' import { useDepositList } from '@asb/context'
import { WellIcon, WellIconState } from '@components/icons' import { WellIcon, WellIconState } from '@components/icons'
import { DepositDto, WellDto } from '@api' import { DepositDto, WellDto } from '@api'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
@ -10,32 +10,41 @@ import { isRawDate } from '@utils'
import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg' import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg'
import { ReactComponent as ClusterIcon } from '@images/ClusterIcon.svg' import { ReactComponent as ClusterIcon } from '@images/ClusterIcon.svg'
import '@styles/wellTreeSelect.css' import '@styles/components/well_tree_select.css'
/**
* Для поиска в URL текущего раздела по шаблону `/{type}/{id}`
*
* Если найдено совпадение может вернуть 1 или 2 группы соответственно
*/
const URL_REGEX = /^\/([^\/?#]+)(?:\/([^\/?#]+))?/
export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown' export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown'
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean => export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000) isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
const getKeyByUrl = (url?: string): [Key | null, string | null] => { const getKeyByUrl = (url?: string): [Key | null, string | null, number | null] => {
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id" const result = url?.match(URL_REGEX) // pattern "/:type/:id"
if (!result) return [null, null] if (!result) return [null, null, null]
return [result[0], result[1]] return [result[0], result[1], result[2] && result[2] !== 'null' ? Number(result[2]) : null]
} }
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => { const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
const [url, type] = getKeyByUrl(value) const [url, type, key] = getKeyByUrl(value)
if (!url) return if (!url) return
let deposit: TreeDataNode | undefined let deposit: TreeDataNode | undefined
let cluster: TreeDataNode | undefined let cluster: TreeDataNode | undefined
let well: TreeDataNode | undefined let well: TreeDataNode | undefined
switch (type) { switch (type) {
case 'deposit': case 'deposit':
if (key === null) return 'Месторождение не выбрано'
deposit = wellsTree.find((deposit) => deposit.key === url) deposit = wellsTree.find((deposit) => deposit.key === url)
if (deposit) if (deposit)
return `${deposit.title}` return `${deposit.title}`
return 'Ошибка! Месторождение не найдено!' return 'Ошибка! Месторождение не найдено!'
case 'cluster': case 'cluster':
if (key === null) return 'Куст не выбран'
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url) cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url)
)) ))
@ -44,6 +53,7 @@ const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined
return 'Ошибка! Куст не найден!' return 'Ошибка! Куст не найден!'
case 'well': case 'well':
if (key === null) return 'Скважина не выбрана'
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeDataNode) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => (
well = cluster.children?.find((well: TreeDataNode) => well.key === url) well = cluster.children?.find((well: TreeDataNode) => well.key === url)
@ -126,7 +136,7 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const deposits = useDeposits() const deposits = useDepositList()
const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits]) const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits])
@ -137,8 +147,8 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
}, [wellsTree]) }, [wellsTree])
const onSelect = useCallback((value: Key[]): void => { const onSelect = useCallback((value: Key[]): void => {
const newRoot = /\/(\w+)\/\d+/.exec(String(value)) const newRoot = URL_REGEX.exec(String(value))
const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname) const oldRoot = URL_REGEX.exec(location.pathname)
if (!newRoot || !oldRoot) return if (!newRoot || !oldRoot) return
let newPath = newRoot[0] let newPath = newRoot[0]

View File

@ -9,6 +9,7 @@ export type CompanyViewProps = {
company?: CompanyDto company?: CompanyDto
} }
/** Компонент для отображения информации о компании */
export const CompanyView = memo<CompanyViewProps>(({ company }) => company ? ( export const CompanyView = memo<CompanyViewProps>(({ company }) => company ? (
<Tooltip title={ <Tooltip title={
<Grid style={{ columnGap: '8px' }}> <Grid style={{ columnGap: '8px' }}>

View File

@ -8,6 +8,7 @@ export type PermissionViewProps = {
info?: PermissionDto info?: PermissionDto
} }
/** Компонент для отображения информации о разрешении */
export const PermissionView = memo<PermissionViewProps>(({ info }) => info ? ( export const PermissionView = memo<PermissionViewProps>(({ info }) => info ? (
<Tooltip overlayInnerStyle={{ width: '400px' }} title={ <Tooltip overlayInnerStyle={{ width: '400px' }} title={
<Grid> <Grid>

View File

@ -9,6 +9,7 @@ export type RoleViewProps = {
role?: UserRoleDto role?: UserRoleDto
} }
/** Компонент для отображения информации о роли */
export const RoleView = memo<RoleViewProps>(({ role }) => { export const RoleView = memo<RoleViewProps>(({ role }) => {
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> ) if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )

View File

@ -19,6 +19,12 @@ export const lables: Record<string, string> = {
spinPlcVersion: 'Версия Спин Мастер', spinPlcVersion: 'Версия Спин Мастер',
} }
/**
* Строит название для телеметрии
*
* @param telemetry Объект телеметрии
* @returns Название
*/
export const getTelemetryLabel = (telemetry?: TelemetryDto) => export const getTelemetryLabel = (telemetry?: TelemetryDto) =>
`${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}` `${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}`
@ -26,6 +32,7 @@ export type TelemetryViewProps = {
telemetry?: TelemetryDto telemetry?: TelemetryDto
} }
/** Компонент для отображения информации о телеметрии */
export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? ( export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? (
<Tooltip <Tooltip
overlayInnerStyle={{ width: '400px' }} overlayInnerStyle={{ width: '400px' }}

View File

@ -10,6 +10,7 @@ export type UserViewProps = HTMLProps<HTMLSpanElement> & {
user?: UserDto user?: UserDto
} }
/** Компонент для отображения информации о пользователе */
export const UserView = memo<UserViewProps>(({ user, ...other }) => export const UserView = memo<UserViewProps>(({ user, ...other }) =>
user ? ( user ? (
<Tooltip <Tooltip

View File

@ -1,4 +1,4 @@
import { memo } from 'react' import { DetailedHTMLProps, HTMLAttributes, memo } from 'react'
import { Tooltip, TooltipProps } from 'antd' import { Tooltip, TooltipProps } from 'antd'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
@ -15,9 +15,19 @@ const wellState: Record<number, { enum: WellIconState, label: string }> = {
export type WellViewProps = TooltipProps & { export type WellViewProps = TooltipProps & {
well?: WellDto well?: WellDto
iconProps?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
labelProps?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
} }
export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? ( /**
* Получить название скважины
* @param well Объект с данными скважины
* @returns Название скважины
*/
export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}`
/** Компонент для отображения информации о скважине */
export const WellView = memo<WellViewProps>(({ well, iconProps, labelProps, ...other }) => well ? (
<Tooltip {...other} title={( <Tooltip {...other} title={(
<Grid style={{ columnGap: '8px' }}> <Grid style={{ columnGap: '8px' }}>
<GridItem row={1} col={1}>Название:</GridItem> <GridItem row={1} col={1}>Название:</GridItem>
@ -47,10 +57,12 @@ export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? (
<GridItem row={8} col={2}>{well.id ?? '---'}</GridItem> <GridItem row={8} col={2}>{well.id ?? '---'}</GridItem>
</Grid> </Grid>
)}> )}>
<span role={'img'} style={{ marginRight: 8, lineHeight: 0, verticalAlign: '-0.25em' }}> <span role={'img'} style={{ marginRight: 8, lineHeight: 0, verticalAlign: '-0.25em' }} {...iconProps}>
<WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} /> <WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} />
</span> </span>
{well.caption} <span {...labelProps}>
{getWellTitle(well)}
</span>
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip title={'нет скважины'}>-</Tooltip> <Tooltip title={'нет скважины'}>-</Tooltip>

View File

@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & {
buttonProps?: ButtonProps buttonProps?: ButtonProps
} }
/** Компонент для отображения информации о талевом канате */
export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => ( export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => (
<Tooltip <Tooltip
{...other} {...other}

View File

@ -1,67 +0,0 @@
import { createContext, useContext, useEffect } from 'react'
import { LayoutPortalProps } from '@components/LayoutPortal'
import { DepositDto, UserTokenDto, WellDto } from '@api'
/** Контекст текущей скважины */
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
/** Контекст текущего корневого пути */
export const RootPathContext = createContext<string>('/')
/** Контекст текущего пользователя */
export const UserContext = createContext<UserTokenDto>({})
/** Контекст метода редактирования параметров заголовка и меню */
export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {})
/** Контекст для блока справа от крошек на страницах скважин и админки */
export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {})
/** Контекст со списком месторождений */
export const DepositsContext = createContext<DepositDto[]>([])
/**
* Получить текущую скважину
*
* @returns Текущая скважина, либо `null`
*/
export const useWell = () => useContext(WellContext)
/**
* Получить текущий корневой путь
*
* @returns Текущий корневой путь
*/
export const useRootPath = () => useContext(RootPathContext)
/**
* Получить текущего пользователя
*
* @returns Текущий пользователь, либо `null`
*/
export const useUser = () => useContext(UserContext)
/**
* Получить список скважин
*
* @returns Список скважин
*/
export const useDeposits = () => useContext(DepositsContext)
/**
* Получить метод задания элементов справа от крошек
*
* @returns Метод задания элементов справа от крошек
*/
export const useTopRightBlock = () => useContext(TopRightBlockContext)
/**
* Получить метод задания параметров заголовка и меню
*
* @returns Получить метод задания параметров заголовка и меню
*/
export const useLayoutProps = (props?: LayoutPortalProps) => {
const setLayoutProps = useContext(LayoutPropsContext)
useEffect(() => {
if (props) setLayoutProps(props)
}, [setLayoutProps, props])
return setLayoutProps
}

23
src/context/deposit.ts Normal file
View File

@ -0,0 +1,23 @@
import { createContext, useContext } from 'react'
import { DepositDto } from '@api'
/** Контекст текущего месторождения */
export const DepositContext = createContext<DepositDto | null>(null)
/**
* Получить текущее месторождение
*
* @returns Текущее месторождение, либо `null`
*/
export const useDeposit = () => useContext(DepositContext)
/** Контекст со списком месторождений */
export const DepositListContext = createContext<DepositDto[]>([])
/**
* Получить список скважин
*
* @returns Список скважин
*/
export const useDepositList = () => useContext(DepositListContext)

5
src/context/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './deposit'
export * from './layout_props'
export * from './root_path'
export * from './user'
export * from './well'

View File

@ -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)

11
src/context/root_path.ts Normal file
View File

@ -0,0 +1,11 @@
import { createContext, useContext } from 'react'
/** Контекст текущего корневого пути */
export const RootPathContext = createContext<string>('/')
/**
* Получить текущий корневой путь
*
* @returns Текущий корневой путь
*/
export const useRootPath = () => useContext(RootPathContext)

13
src/context/user.ts Normal file
View File

@ -0,0 +1,13 @@
import { createContext, useContext } from 'react'
import { UserTokenDto } from '@api'
/** Контекст текущего пользователя */
export const UserContext = createContext<UserTokenDto>({})
/**
* Получить текущего пользователя
*
* @returns Текущий пользователь, либо `null`
*/
export const useUser = () => useContext(UserContext)

13
src/context/well.ts Normal file
View File

@ -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)

40
src/images/AsbLogo.svg Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 1800 8000 4000" version="1.1" alt="АСБ" fill="#2d2242">
<defs>
<clipPath id="logo-clip-path-id0">
<path d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
</clipPath>
</defs>
<g transform="translate(-982.80503,2106.4728)">
<g fill="#e31e24">
<path d="M 1756,3564 H 1018 L 3236,2 3848,3 4452,0 4400,184 C 4637,66 4905,0 5189,0 5751,0 6253,261 6579,669 l -233,810 C 6213,964 5745,584 5189,584 c -528,0 -975,341 -1134,815 l -30,108 c -20,87 -31,178 -31,272 0,660 535,1195 1195,1195 318,0 607,-125 821,-327 l -220,764 c -185,88 -388,147 -601,147 -636,0 -1194,-334 -1508,-836 l -239,842 h -702 l 187,-595 H 2146 Z M 3082,2443 3703,446 2463,2444 Z" />
<path d="m 7725,3574 c -534.9685,-1.0406 -1176.3914,-0.3681 -1863.0925,-0.3084 L 6882,2 l 1790,1 -136,559 -1176,9 -121,462 h 836 c 570,93 953,697 950,1254 -3,656 -585,1291 -1300,1287 z m -995,-606 c 333,0 665,0 998,2 381,2 691,-335 693,-686 1,-291 -206,-632 -510,-673 h -824 z"/>
</g>
<path d="m 5347,1437 h -242 v -122 h 242 z" />
<path d="m 5455,1555 h -463 v -86 h 463 z" />
<path d="m 5597,2523 h -737 l 167,-936 h 392 z" />
<path d="m 5246,2523 h -46 v -788 h 46 z" />
<g fill="#fefefe">
<path d="m 5166,1737 -105,93 28,-154 z" />
<path d="m 5288,1737 105,93 -28,-154 z" />
<path d="m 5224,1696 61,-42 h -113 z" />
<path d="m 5143,2007 -124,55 20,-110 z" />
<path d="m 5310,2007 125,55 -20,-110 z" />
<path d="m 5091,1894 138,68 136,-68 -136,-111 z" />
<path d="m 5052,2132 180,119 180,-121 -183,-87 z" />
<path d="m 5163,2297 -214,148 47,-261 z" />
<path d="m 5292,2297 213,148 -47,-261 z" />
<path d="m 5226,2337 271,186 h -539 z" />
</g>
<g clip-path="url(#logo-clip-path-id0)">
<g fill="9d9e9e">
<path d="m 5136,177 c -688,66 -1152,378 -1415,911 l 1475,-196 783,591 z" />
<path d="M 6684,1229 C 6401,599 5957,260 5367,182 l 659,1333 -308,931 z" />
</g>
<path d="m 6189,3044 c 509,-466 692,-994 581,-1579 l -1059,1044 -981,-1 z" />
<path d="m 4267,3105 c 598,345 1157,360 1681,78 L 4633,2488 4337,1552 Z" />
<path d="m 3626,1346 c -142,676 17,1212 447,1622 l 253,-1466 798,-571 z" />
</g>
<path fill="none" d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

33
src/images/Logo.svg Normal file
View File

@ -0,0 +1,33 @@
<svg version="1.1" viewBox="0 0 896 282" fill="#f3f6e8" className="logo" overflow="visible">
<g className="logo-icon">
<path fill="#9e1937" d="m126 32.2h-92.5c-2.58 0-4.67-2.09-4.67-4.67s2.09-4.67 4.67-4.67h92.5c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67" />
<path d="m30.5 274h98.3l-36.1-194h-26.2zm104 9.33h-110c-1.39 0-2.7-0.617-3.59-1.68-0.887-1.07-1.25-2.47-0.999-3.83l37.8-203c0.41-2.21 2.34-3.82 4.59-3.82h34c2.25 0 4.18 1.6 4.59 3.82l37.8 203c0.253 1.36-0.112 2.77-0.999 3.83-0.887 1.07-2.2 1.68-3.59 1.68" />
<path d="m113 10.3h-66.9c-2.58 0-4.67-2.09-4.67-4.67 0-2.58 2.09-4.67 4.67-4.67h66.9c2.58 0 4.67 2.09 4.67 4.67 0 2.58-2.09 4.67-4.67 4.67" />
<path d="m155 262c-2.17 0-4.12-1.53-4.57-3.74l-41.1-203h-58.8l-39.9 197h85.9l-44.2-33.2c-1.61-1.21-2.26-3.3-1.62-5.21 0.635-1.91 2.42-3.19 4.43-3.19h37.1l-34.6-28.7c-1.51-1.26-2.08-3.33-1.41-5.17 0.668-1.85 2.42-3.08 4.39-3.08h27.8l-25.3-25.5c-1.33-1.34-1.72-3.34-1-5.08 0.725-1.74 2.42-2.87 4.31-2.87h18.3l-16.8-19c-1.22-1.37-1.51-3.33-0.759-5.01 0.754-1.67 2.42-2.75 4.25-2.75h17.6c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67h-7.23l16.8 19c1.22 1.38 1.51 3.34 0.759 5.01-0.754 1.67-2.42 2.75-4.25 2.75h-17.4l25.3 25.5c1.33 1.34 1.72 3.34 1 5.08-0.724 1.74-2.42 2.87-4.31 2.87h-26.1l34.6 28.7c1.51 1.26 2.08 3.33 1.41 5.17-0.668 1.85-2.42 3.08-4.39 3.08h-36.1l44.2 33.2c1.61 1.21 2.26 3.3 1.62 5.21-0.635 1.91-2.42 3.19-4.43 3.19h-106c-1.4 0-2.73-0.629-3.61-1.71-0.886-1.09-1.24-2.51-0.961-3.88l41.8-206c0.441-2.18 2.35-3.74 4.57-3.74h66.5c2.22 0 4.13 1.56 4.57 3.74l41.8 206c0.512 2.53-1.12 4.99-3.65 5.5-0.312 0.0625-0.624 0.0948-0.932 0.0948" />
</g>
<g className="logo-label">
<path fill="#9e1937" d="m316 140c2.76-2.67 5.01-1.71 5.01 2.13v30.3c0 3.84-3.14 6.98-6.98 6.98h-2.38c-3.84 0-6.98-3.14-6.98-6.98v-14.5c0-3.84 2.26-9.16 5.01-11.8l6.31-6.09" />
<path d="m647 159c0 3.84-3.14 6.98-6.97 6.98h-3.84c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v118" />
<path d="m707 144c0 3.84 3.14 6.97 6.98 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v102" />
<path d="m827 144c0 3.84 3.14 6.97 6.97 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.98-3.14-6.98-6.98v-118c0-3.84 3.14-6.98 6.98-6.98h3.84c3.84 0 6.98 3.14 6.98 6.98v102" />
<path d="m279 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1" />
<path d="m432 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1" />
<path d="m539 94.6h-33c-3.84 0-6.98-3.14-6.98-6.98v-30.9c0-3.84 3.14-6.98 6.98-6.98h36.3c8.89 0 23.6 1.63 23.6 22 0 19.4-13.8 22.9-26.9 22.9zm26.9 6.9c8.35-4.9 18.3-12.2 18.3-31.6 0-27.8-21.8-35.4-43.4-35.4h-52.6c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.97 6.98 6.97h3.84c3.84 0 6.97-3.14 6.97-6.97v-42.5c0-3.84 3.14-6.98 6.98-6.98h34.9c21.4 0 23.6 12.5 23.6 23.4 0.308 6.29 0 16.5 0 23.9s3.76 9.1 8.94 9.1h8.85v-40.1c0-18.5-10.3-20.7-16.3-24.7" />
<path d="m220 256c-0.964 0-1.77-0.809-1.77-1.77v-2.85h-17.7c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v19.4h9.25v-19.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v19.4h1.89c0.962 0 1.77 0.807 1.77 1.77v6.86c0 0.962-0.809 1.77-1.77 1.77h-2.23" />
<path d="m267 251c-0.964 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.2 1.12-2.01 1.12h-2.39c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.19-1.12 2-1.12h2.39c0.964 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62" />
<path d="m319 238c0-2.54-1.39-4.78-5.51-4.89v9.78c4.04-0.076 5.51-1.73 5.51-4.89zm-17.1 0c0 3.12 1.66 4.81 5.43 4.89v-9.78c-3.93 0.115-5.43 2.27-5.43 4.89zm11.6 12.4c0 0.965-0.807 1.77-1.77 1.77h-2.62c-0.962 0-1.77-0.809-1.77-1.77v-1.85c-7.2-0.0758-12-3.82-12-10.6 0-6.66 4.89-10.4 12-10.6v-1.08c0-0.965 0.812-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v1.08c7.01 0.158 12.1 3.97 12.1 10.6 0 6.7-4.89 10.5-12.1 10.6v1.85" />
<path d="m356 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.576 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94" />
<path d="m405 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9" />
<path d="m450 246c0.423 0.115 0.923 0.232 2.08 0.232 2.39 0 3.58-1.04 3.58-2.85 0-1.7-1.27-2.43-3.27-2.43h-2.39zm2.04-10.4c1.58 0 2.85-0.654 2.85-2.62 0-1.62-1.46-2.43-2.96-2.43-0.77 0-1.23 0.0768-1.93 0.156v4.89zm5.93 1.93c1.96 0.771 3.85 2.73 3.85 6.2 0 5.66-4.39 8.32-10.2 8.32-1.85 0-4.2-0.0364-5.97-0.113-0.925-0.0393-1.77-0.923-1.77-1.85v-23.3c0-0.964 0.809-1.81 1.77-1.85 1.81-0.0759 4.32-0.155 6.39-0.155 6.43 0 9.05 2.97 9.05 6.7 0 2.81-1.08 4.7-3.08 6.05" />
<path d="m500 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9" />
<path d="m555 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23" />
<path d="m613 246c1 0.116 1.62 0.192 2.31 0.192 2.54 0 3.35-1.27 3.35-2.78 0-1.58-0.849-3-3.08-3-0.77 0-1.66 0.077-2.58 0.232zm3.08-11.4c5.2 0 8.74 3.24 8.74 8.32 0 5.62-3.74 9.01-10.5 9.01-2.39 0-4.28-0.0758-5.74-0.153-1-0.0393-1.77-0.846-1.77-1.85v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.7c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.32v4.28c0.733-0.157 2.23-0.233 3.08-0.233" />
<path d="m661 238 4.93-11.9c0.347-0.771 1.08-1.27 1.89-1.27h2.2c1.04 0 1.73 0.733 1.73 1.66 0 0.268-0.076 0.538-0.192 0.807l-7.93 18.1c-1.81 4.12-4.16 6.39-7.9 6.39-1.04 0-2.12-0.191-3.16-0.654-0.541-0.268-0.886-0.733-0.886-1.42 0-0.233 0.037-0.502 0.153-0.809l0.733-1.89c0.347-0.923 0.925-1.2 1.62-1.2 0.231 0 0.463 0.0393 0.694 0.0759 0.502 0.118 0.846 0.118 1 0.118 0.809 0 1.46-0.31 1.81-1.08l0.347-0.809-9.98-16.6c-0.192-0.35-0.268-0.697-0.268-1.04 0-0.889 0.615-1.66 1.69-1.66h2.58c0.807 0 1.62 0.462 2.04 1.19l6.89 12" />
<path d="m702 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.578 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94" />
<path d="m756 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23" />
<path d="m803 250c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-9.09h-9.82v9.09c0 0.965-0.812 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v8.55h9.82v-8.55c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v23.4" />
<path d="m849 251c-0.962 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.19 1.12-2 1.12h-2.39c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.2-1.12 2-1.12h2.39c0.962 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62" />
<path d="m896 250c0 0.965-0.809 1.77-1.77 1.77h-12.8c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.962 0 1.77 0.807 1.77 1.77v2.23" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,44 +1,14 @@
import { memo } from 'react' import { memo } from 'react'
import { ReactComponent as RawLogo } from './Logo.svg'
export type LogoProps = React.SVGProps<SVGSVGElement> & { export type LogoProps = React.SVGProps<SVGSVGElement> & {
size?: number size?: number
onlyIcon?: boolean onlyIcon?: boolean
} }
export const Logo = memo<LogoProps>(({ size = 170, onlyIcon, ...props }) => ( export const Logo = memo<LogoProps>(({ size = 170, onlyIcon, ...props }) => (
<svg version={'1.1'} viewBox={`0 0 896 282`} fill={'#f3f6e8'} className={'logo'} style={{ width: size, height: 282/896*size, overflow: 'visible' }} {...props}> <RawLogo style={{ width: size, height: 282/896*size }} {...props} />
<g className={'logo-icon'}>
<path fill={'#9e1937'} d={'m126 32.2h-92.5c-2.58 0-4.67-2.09-4.67-4.67s2.09-4.67 4.67-4.67h92.5c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67'} />
<path d={'m30.5 274h98.3l-36.1-194h-26.2zm104 9.33h-110c-1.39 0-2.7-0.617-3.59-1.68-0.887-1.07-1.25-2.47-0.999-3.83l37.8-203c0.41-2.21 2.34-3.82 4.59-3.82h34c2.25 0 4.18 1.6 4.59 3.82l37.8 203c0.253 1.36-0.112 2.77-0.999 3.83-0.887 1.07-2.2 1.68-3.59 1.68'} />
<path d={'m113 10.3h-66.9c-2.58 0-4.67-2.09-4.67-4.67 0-2.58 2.09-4.67 4.67-4.67h66.9c2.58 0 4.67 2.09 4.67 4.67 0 2.58-2.09 4.67-4.67 4.67'} />
<path d={'m155 262c-2.17 0-4.12-1.53-4.57-3.74l-41.1-203h-58.8l-39.9 197h85.9l-44.2-33.2c-1.61-1.21-2.26-3.3-1.62-5.21 0.635-1.91 2.42-3.19 4.43-3.19h37.1l-34.6-28.7c-1.51-1.26-2.08-3.33-1.41-5.17 0.668-1.85 2.42-3.08 4.39-3.08h27.8l-25.3-25.5c-1.33-1.34-1.72-3.34-1-5.08 0.725-1.74 2.42-2.87 4.31-2.87h18.3l-16.8-19c-1.22-1.37-1.51-3.33-0.759-5.01 0.754-1.67 2.42-2.75 4.25-2.75h17.6c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67h-7.23l16.8 19c1.22 1.38 1.51 3.34 0.759 5.01-0.754 1.67-2.42 2.75-4.25 2.75h-17.4l25.3 25.5c1.33 1.34 1.72 3.34 1 5.08-0.724 1.74-2.42 2.87-4.31 2.87h-26.1l34.6 28.7c1.51 1.26 2.08 3.33 1.41 5.17-0.668 1.85-2.42 3.08-4.39 3.08h-36.1l44.2 33.2c1.61 1.21 2.26 3.3 1.62 5.21-0.635 1.91-2.42 3.19-4.43 3.19h-106c-1.4 0-2.73-0.629-3.61-1.71-0.886-1.09-1.24-2.51-0.961-3.88l41.8-206c0.441-2.18 2.35-3.74 4.57-3.74h66.5c2.22 0 4.13 1.56 4.57 3.74l41.8 206c0.512 2.53-1.12 4.99-3.65 5.5-0.312 0.0625-0.624 0.0948-0.932 0.0948'} />
</g>
<g className={'logo-label'}>
<path fill={'#9e1937'} d={'m316 140c2.76-2.67 5.01-1.71 5.01 2.13v30.3c0 3.84-3.14 6.98-6.98 6.98h-2.38c-3.84 0-6.98-3.14-6.98-6.98v-14.5c0-3.84 2.26-9.16 5.01-11.8l6.31-6.09'} />
<path d={'m647 159c0 3.84-3.14 6.98-6.97 6.98h-3.84c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v118'} />
<path d={'m707 144c0 3.84 3.14 6.97 6.98 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v102'} />
<path d={'m827 144c0 3.84 3.14 6.97 6.97 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.98-3.14-6.98-6.98v-118c0-3.84 3.14-6.98 6.98-6.98h3.84c3.84 0 6.98 3.14 6.98 6.98v102'} />
<path d={'m279 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1'} />
<path d={'m432 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1'} />
<path d={'m539 94.6h-33c-3.84 0-6.98-3.14-6.98-6.98v-30.9c0-3.84 3.14-6.98 6.98-6.98h36.3c8.89 0 23.6 1.63 23.6 22 0 19.4-13.8 22.9-26.9 22.9zm26.9 6.9c8.35-4.9 18.3-12.2 18.3-31.6 0-27.8-21.8-35.4-43.4-35.4h-52.6c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.97 6.98 6.97h3.84c3.84 0 6.97-3.14 6.97-6.97v-42.5c0-3.84 3.14-6.98 6.98-6.98h34.9c21.4 0 23.6 12.5 23.6 23.4 0.308 6.29 0 16.5 0 23.9s3.76 9.1 8.94 9.1h8.85v-40.1c0-18.5-10.3-20.7-16.3-24.7'} />
<path d={'m220 256c-0.964 0-1.77-0.809-1.77-1.77v-2.85h-17.7c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v19.4h9.25v-19.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v19.4h1.89c0.962 0 1.77 0.807 1.77 1.77v6.86c0 0.962-0.809 1.77-1.77 1.77h-2.23'} />
<path d={'m267 251c-0.964 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.2 1.12-2.01 1.12h-2.39c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.19-1.12 2-1.12h2.39c0.964 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62'} />
<path d={'m319 238c0-2.54-1.39-4.78-5.51-4.89v9.78c4.04-0.076 5.51-1.73 5.51-4.89zm-17.1 0c0 3.12 1.66 4.81 5.43 4.89v-9.78c-3.93 0.115-5.43 2.27-5.43 4.89zm11.6 12.4c0 0.965-0.807 1.77-1.77 1.77h-2.62c-0.962 0-1.77-0.809-1.77-1.77v-1.85c-7.2-0.0758-12-3.82-12-10.6 0-6.66 4.89-10.4 12-10.6v-1.08c0-0.965 0.812-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v1.08c7.01 0.158 12.1 3.97 12.1 10.6 0 6.7-4.89 10.5-12.1 10.6v1.85'} />
<path d={'m356 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.576 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94'} />
<path d={'m405 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9'} />
<path d={'m450 246c0.423 0.115 0.923 0.232 2.08 0.232 2.39 0 3.58-1.04 3.58-2.85 0-1.7-1.27-2.43-3.27-2.43h-2.39zm2.04-10.4c1.58 0 2.85-0.654 2.85-2.62 0-1.62-1.46-2.43-2.96-2.43-0.77 0-1.23 0.0768-1.93 0.156v4.89zm5.93 1.93c1.96 0.771 3.85 2.73 3.85 6.2 0 5.66-4.39 8.32-10.2 8.32-1.85 0-4.2-0.0364-5.97-0.113-0.925-0.0393-1.77-0.923-1.77-1.85v-23.3c0-0.964 0.809-1.81 1.77-1.85 1.81-0.0759 4.32-0.155 6.39-0.155 6.43 0 9.05 2.97 9.05 6.7 0 2.81-1.08 4.7-3.08 6.05'} />
<path d={'m500 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9'} />
<path d={'m555 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23'} />
<path d={'m613 246c1 0.116 1.62 0.192 2.31 0.192 2.54 0 3.35-1.27 3.35-2.78 0-1.58-0.849-3-3.08-3-0.77 0-1.66 0.077-2.58 0.232zm3.08-11.4c5.2 0 8.74 3.24 8.74 8.32 0 5.62-3.74 9.01-10.5 9.01-2.39 0-4.28-0.0758-5.74-0.153-1-0.0393-1.77-0.846-1.77-1.85v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.7c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.32v4.28c0.733-0.157 2.23-0.233 3.08-0.233'} />
<path d={'m661 238 4.93-11.9c0.347-0.771 1.08-1.27 1.89-1.27h2.2c1.04 0 1.73 0.733 1.73 1.66 0 0.268-0.076 0.538-0.192 0.807l-7.93 18.1c-1.81 4.12-4.16 6.39-7.9 6.39-1.04 0-2.12-0.191-3.16-0.654-0.541-0.268-0.886-0.733-0.886-1.42 0-0.233 0.037-0.502 0.153-0.809l0.733-1.89c0.347-0.923 0.925-1.2 1.62-1.2 0.231 0 0.463 0.0393 0.694 0.0759 0.502 0.118 0.846 0.118 1 0.118 0.809 0 1.46-0.31 1.81-1.08l0.347-0.809-9.98-16.6c-0.192-0.35-0.268-0.697-0.268-1.04 0-0.889 0.615-1.66 1.69-1.66h2.58c0.807 0 1.62 0.462 2.04 1.19l6.89 12'} />
<path d={'m702 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.578 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94'} />
<path d={'m756 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23'} />
<path d={'m803 250c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-9.09h-9.82v9.09c0 0.965-0.812 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v8.55h9.82v-8.55c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v23.4'} />
<path d={'m849 251c-0.962 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.19 1.12-2 1.12h-2.39c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.2-1.12 2-1.12h2.39c0.962 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62'} />
<path d={'m896 250c0 0.965-0.809 1.77-1.77 1.77h-12.8c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.962 0 1.77 0.807 1.77 1.77v2.23'} />
</g>
</svg>
)) ))
export default Logo export default Logo

View File

@ -12,7 +12,7 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' import { makeItem, PrivateMenu } from '@components/PrivateMenu'
import { isDev } from '@utils' import { isDev } from '@utils'
export const menuItems = [ export const menuItems = [
@ -33,7 +33,7 @@ export const menuItems = [
].filter(Boolean) ].filter(Boolean)
export const AdminNavigationMenu = memo((props) => ( export const AdminNavigationMenu = memo((props) => (
<PrivateWellMenu <PrivateMenu
{...props} {...props}
items={menuItems} items={menuItems}
rootPath={'/admin'} rootPath={'/admin'}

View File

@ -3,11 +3,12 @@ import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
makeColumn,
makeSelectColumn, makeSelectColumn,
makeStringSorter, makeStringSorter,
defaultPagination, defaultPagination,
makeTimezoneColumn makeTimezoneColumn,
makeNumericColumn,
makeTextColumn
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api' import { AdminClusterService, AdminDepositService } from '@api'
@ -30,17 +31,11 @@ const ClusterController = memo(() => {
const clusterColumns = useMemo(() => [ const clusterColumns = useMemo(() => [
makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', {
width: 200, width: 200,
editable: true,
sorter: makeStringSorter('idDeposit') sorter: makeStringSorter('idDeposit')
}), }),
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200, makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
editable: true, makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
], [deposits]) ], [deposits])

View File

@ -3,10 +3,9 @@ import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
makeColumn,
makeStringSorter,
makeSelectColumn, makeSelectColumn,
defaultPagination defaultPagination,
makeTextColumn
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api' import { AdminCompanyService, AdminCompanyTypeService } from '@api'
@ -37,16 +36,8 @@ const CompanyController = memo(() => {
})) }))
setColumns([ setColumns([
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200, makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { width: 200 }),
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, {
width: 200,
editable: true
}),
]) ])
await updateTable() await updateTable()

View File

@ -1,19 +1,14 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' 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 { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminCompanyTypeService } from '@api' import { AdminCompanyTypeService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200,
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
] ]
const CompanyTypeController = memo(() => { const CompanyTypeController = memo(() => {

View File

@ -2,15 +2,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' 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 { arrayOrDefault, coordsFormat, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminDepositService } from '@api' import { AdminDepositService } from '@api'
const depositColumns = [ const depositColumns = [
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]

View File

@ -1,23 +1,15 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { EditableTable, makeColumn, makeStringSorter } from '@components/Table' import { EditableTable, makeTextColumn } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminPermissionService } from '@api' import { AdminPermissionService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'name', { makeTextColumn('Название', 'name', undefined, undefined, undefined, { isRequired: true, formItemRules: min1 }),
editable: true, makeTextColumn('Описание', 'description'),
sorter: makeStringSorter('name'),
isRequired: true,
formItemRules: min1,
}),
makeColumn('Описание', 'description', {
editable: true,
sorter: makeStringSorter('description'),
}),
] ]
const PermissionController = memo(() => { const PermissionController = memo(() => {

View File

@ -19,15 +19,13 @@ const RoleController = memo(() => {
)), [roles, searchValue]) )), [roles, searchValue])
const columns = useMemo(() => [ 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', { makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
width: 400, width: 400,
editable: true,
render: (role) => <RoleView role={role} /> render: (role) => <RoleView role={role} />
}, { allowClear: true }), }, { allowClear: true }),
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
width: 600, width: 600,
editable: true,
render: (permission) => <PermissionView info={permission} />, render: (permission) => <PermissionView info={permission} />,
}), }),
], [roles, permissions]) ], [roles, permissions])

View File

@ -50,7 +50,7 @@ const TelemetryController = memo(() => {
const columns = useMemo(() => [ const columns = useMemo(() => [
makeColumn('', 'hasParent', { render: mergeRender }), makeColumn('', 'hasParent', { render: mergeRender }),
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), makeNumericColumn('ID', 'id', makeNumericRender(0)),
makeTextColumn('UID', 'remoteUid'), makeTextColumn('UID', 'remoteUid'),
makeTextColumn('Назначена на скважину', 'realWell'), makeTextColumn('Назначена на скважину', 'realWell'),
makeDateColumn('Дата начала бурения', 'drillingStartDate'), makeDateColumn('Дата начала бурения', 'drillingStartDate'),

View File

@ -115,7 +115,6 @@ const UserController = memo(() => {
setColumns([ setColumns([
makeTextColumn('Логин', 'login', null, null, null, { makeTextColumn('Логин', 'login', null, null, null, {
editable: true,
formItemRules: [ formItemRules: [
{ required: true }, { required: true },
...createLoginRules, ...createLoginRules,
@ -130,41 +129,34 @@ const UserController = memo(() => {
], ],
}), }),
makeTextColumn('Фамилия', 'surname', filters.surname, null, null, { makeTextColumn('Фамилия', 'surname', filters.surname, null, null, {
editable: true,
formItemRules: [{ required: true }, ...nameRules], formItemRules: [{ required: true }, ...nameRules],
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('surname'), onFilter: makeTextOnFilter('surname'),
}), }),
makeTextColumn('Имя', 'name', filters.name, null, null, { makeTextColumn('Имя', 'name', filters.name, null, null, {
editable: true,
formItemRules: nameRules, formItemRules: nameRules,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('name'), onFilter: makeTextOnFilter('name'),
}), }),
makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, { makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, {
editable: true,
formItemRules: nameRules, formItemRules: nameRules,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('patronymic'), onFilter: makeTextOnFilter('patronymic'),
}), }),
makeTextColumn('E-mail', 'email', filters.email, null, null, { makeTextColumn('E-mail', 'email', filters.email, null, null, {
editable: true,
formItemRules: [{ required: true }, ...emailRules], formItemRules: [{ required: true }, ...emailRules],
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('email'), onFilter: makeTextOnFilter('email'),
}), }),
makeTextColumn('Номер телефона', 'phone', null, null, null, { makeTextColumn('Номер телефона', 'phone', null, null, null, {
editable: true,
formItemRules: phoneRules, formItemRules: phoneRules,
}), }),
makeTextColumn('Должность', 'position', null, null, null, { editable: true }), makeTextColumn('Должность', 'position', null, null, null),
makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
onFilter: makeArrayOnFilter('roleNames'), onFilter: makeArrayOnFilter('roleNames'),
}), }),
makeSelectColumn('Компания', 'idCompany', companies, '--', { makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true,
sorter: makeNumericSorter('idCompany'), sorter: makeNumericSorter('idCompany'),
}) })
]) ])

View File

@ -12,11 +12,12 @@ import {
EditableTable, EditableTable,
makeColumn, makeColumn,
makeSelectColumn, makeSelectColumn,
makeStringSorter,
makeNumericSorter, makeNumericSorter,
makeTagColumn, makeTagColumn,
defaultPagination, defaultPagination,
makeTimezoneColumn, makeTimezoneColumn,
makeTextColumn,
makeNumericColumn,
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views' import { TelemetryView, CompanyView } from '@components/views'
@ -81,23 +82,11 @@ const WellController = memo(() => {
})) }))
setColumns([ setColumns([
makeSelectColumn('Куст', 'idCluster', clusters, '--', { makeSelectColumn('Куст', 'idCluster', clusters, '--', { width: '5rem', sorter: makeNumericSorter('idCluster') }),
width: '5rem', makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: '5rem' }),
editable: true, makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { width: 150, sorter: makeNumericSorter('idWellType') }),
sorter: makeNumericSorter('idCluster'), makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
}), makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
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 }),
makeColumn('Телеметрия', 'telemetry', { makeColumn('Телеметрия', 'telemetry', {
editable: true, editable: true,
render: (telemetry) => <TelemetryView telemetry={telemetry} />, render: (telemetry) => <TelemetryView telemetry={telemetry} />,
@ -105,7 +94,6 @@ const WellController = memo(() => {
}, ), }, ),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
editable: true,
render: (company) => <CompanyView company={company} />, render: (company) => <CompanyView company={company} />,
}), }),
]) ])

View File

@ -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 { lazy, memo, useEffect, useMemo } from 'react'
import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context' import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context'
import { FastRunMenu } from '@components/FastRunMenu' import { FastRunMenu } from '@components/FastRunMenu'
import { makeMenuBreadcrumbItems } from '@components/MenuBreadcrumb' import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb'
import { NoAccessComponent, withPermissions } from '@utils' import { NoAccessComponent, withPermissions } from '@utils'
import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu' import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu'
@ -21,21 +21,18 @@ const TelemetryViewer = lazy(() => import('./Telemetry/TelemetryViewer'))
const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger')) const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger'))
const VisitLog = lazy(() => import('./VisitLog')) const VisitLog = lazy(() => import('./VisitLog'))
const AdminPanel = memo(() => { const layoutProps = {
const location = useLocation()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/admin`, [root])
const setLayoutProps = useLayoutProps()
useEffect(() => {
setLayoutProps({
sider: <AdminNavigationMenu />, sider: <AdminNavigationMenu />,
title: 'Администраторская панель', title: 'Администраторская панель',
isAdmin: true, isAdmin: true,
breadcrumb: makeMenuBreadcrumbItems(menuItems, location.pathname, /^\/admin\//), breadcrumb: makeMenuBreadcrumbItemsRender(menuItems, /^\/admin\//),
}) }
}, [location.pathname])
const AdminPanel = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/admin`, [root])
useLayoutProps(layoutProps)
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>

View File

@ -7,7 +7,7 @@ import {
makeTextColumn, makeTextColumn,
makeGroupColumn, makeGroupColumn,
makeColumn, makeColumn,
makeNumericColumnPlanFact, makeNumericColumnPlanFactOld,
Table, Table,
makeNumericRender, makeNumericRender,
makeNumericColumn, makeNumericColumn,
@ -117,7 +117,10 @@ const ClusterWells = memo(({ statsWells }) => {
const columns = useMemo(() => [ const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(_, item) => ( (_, item) => (
<Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}> <Link
to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}}
style={{ display: 'flex', alignItems: 'center' }}
>
<PointerIcon <PointerIcon
state={item.idState === 1 ? 'active' : 'unknown'} state={item.idState === 1 ? 'active' : 'unknown'}
width={32} width={32}
@ -133,10 +136,10 @@ const ClusterWells = memo(({ statsWells }) => {
makeDateColumn('начало', 'factStart'), makeDateColumn('начало', 'factStart'),
makeDateColumn('окончание', 'factEnd'), makeDateColumn('окончание', 'factEnd'),
]), ]),
makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('Продолжительность, сут', 'period', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('МСП, м/ч', 'rateOfPenetration', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'routeSpeed', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => ( makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => (
<Button onClick={() => { <Button onClick={() => {
setSelectedWell(value) setSelectedWell(value)

View File

@ -1,14 +1,14 @@
import { memo, useMemo } from 'react' import { memo, useMemo } from 'react'
import { Table, makeTextColumn, makeNumericColumnPlanFact } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFactOld } from '@components/Table'
import { getPrecision } from '@utils/functions' import { getPrecision } from '@utils/functions'
const columns = [ const columns = [
makeTextColumn('Конструкция секции', 'sectionType'), makeTextColumn('Конструкция секции', 'sectionType'),
makeTextColumn('Операция', 'operationName'), makeTextColumn('Операция', 'operationName'),
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, (number) => getPrecision(number)), makeNumericColumnPlanFactOld('Глубина забоя', 'depth', (number) => getPrecision(number)),
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, (number) => getPrecision(number)), makeNumericColumnPlanFactOld('Часы', 'durationHours', (number) => getPrecision(number)),
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-') makeNumericColumnPlanFactOld('Комментарий', 'comment', (text) => text ?? '-'),
] ]
export const WellOperationsTable = memo(({ wellOperations }) => { export const WellOperationsTable = memo(({ wellOperations }) => {

View File

@ -1,106 +0,0 @@
import { useEffect, memo, useMemo, useCallback } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Map, Overlay } from 'pigeon-maps'
import { Popover, Badge } from 'antd'
import { useDeposits, useLayoutProps } from '@asb/context'
import { PointerIcon } from '@components/icons'
import { FastRunMenu } from '@components/FastRunMenu'
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 Deposit = memo(() => {
const deposits = useDeposits()
const setLayoutProps = useLayoutProps()
const location = useLocation()
const makeDepositLinks = useCallback((clusters) => (
<div>
{clusters.map(cluster => (
<Link
key={cluster.id}
to={{
pathname: `/cluster/${cluster.id}`,
state: { from: location.pathname }
}}
>
<div>{cluster.caption}</div>
</Link>
))}
</div>
), [location.pathname])
const viewParams = useMemo(() => calcViewParams(deposits), [deposits])
useEffect(() => {
const hasId = location.pathname.length > '/deposit/'.length
const selectorProps = {
expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined,
}
setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
})
}, [setLayoutProps, location.pathname])
return (
<>
<FastRunMenu />
<div className={'deposit-page'}>
<Map {...viewParams}>
{deposits.map(deposit => {
const anchor = [deposit.latitude, deposit.longitude]
const links = makeDepositLinks(deposit.clusters)
return (
<Overlay width={32} anchor={anchor} key={anchor.join(' ')}>
<Popover content={links} trigger={['click']} title={deposit.caption}>
<div className={'pointer'}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
)
})}
</Map>
</div>
</>
)
})
export default withPermissions(Deposit, ['Cluster.get'])

View File

@ -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', [], <HeatMapOutlined />),
makeItem('Наработка АКБ', 'statistics_adw', [], <FundOutlined />),
]
export const DepositNavigationMenu = memo((props) => (
<PrivateMenu
{...props}
items={menuItems}
rootPath={'/deposit/{idDeposit}'}
mode={'inline'}
theme={'dark'}
style={{ backgroundColor: 'transparent' }}
/>
))
export default DepositNavigationMenu

93
src/pages/Deposit/Map.jsx Normal file
View File

@ -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) => (
<div>
{clusters.map(cluster => (
<Link
key={cluster.id}
to={{
pathname: `/cluster/${cluster.id}`,
state: { from: location.pathname }
}}
>
<div>{cluster.caption}</div>
</Link>
))}
</div>
), [location.pathname])
const viewParams = useMemo(() => calcViewParams(deposits), [deposits])
return (
<div className={'deposit-page'}>
<PigeonMap {...viewParams}>
{deposits.map(deposit => {
const anchor = [deposit.latitude, deposit.longitude]
const links = makeDepositLinks(deposit.clusters)
return (
<Overlay width={32} anchor={anchor} key={anchor.join(' ')}>
<Popover
content={links}
trigger={['click']}
title={(
<Link to={{ pathname: `/deposit/${deposit.id}` }}>
{deposit.caption}
</Link>
)}
>
<div className={'pointer'}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
)
})}
</PigeonMap>
</div>
)
})
export default withPermissions(Map, ['Cluster.get'])

View File

@ -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 <CheckOutlined />
case 'warn': return <WarningOutlined />
case 'error': return <StopOutlined />
default: return <QuestionCircleOutlined />
}
}
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 (
<div className={`subsystem-status status-${state}`}>
{getSubsystemIcon(state)}
<span key={subsystem.key}>{subsystem.subsystemName || subsystem.key}</span>
</div>
)
}
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 = (
<Card className={`subsystem-card status-${state}`} key={row.well.id} title={getWellTitle(row.well)}>
<div className={'subsystem-card-body'}>
{state ? row.subsystems.map((ss) => generateSubsystem(ss)) : <Empty />}
</div>
</Card>
)
if (!state) return card
return (
<Popover
title={(
<>
<span style={{ paddingRight: 15 }}>Детальная информация по скважине</span>
<WellView well={row.well} />
</>
)}
content={(
<Table
size={'small'}
pagination={false}
dataSource={row.subsystems}
columns={columns}
onRow={onRow}
/>
)}
>{card}</Popover>
)
}), [data])
return (
<LoaderPortal show={isLoading} style={{ flex: 1 }}>
<div className={'statistics-adw-page'}>
<div className={'filter-block'}>
<span>Диапазон дат:</span>
<DateRangeWrapper allowClear onChange={setDates} value={dates} />
</div>
<div className={'well-cards'}>{cards}</div>
</div>
</LoaderPortal>
)
})
export default withPermissions(GeneralSubsystemStatistics, [])

View File

@ -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: <DepositNavigationMenu variables={{ idDeposit: idDeposit }} />,
showSelector: isMap,
selectorProps,
title: 'Месторождение',
})
}, [setLayoutProps, idDeposit, isMap])
return (
<RootPathContext.Provider value={rootPath}>
<DepositContext.Provider value={deposit}>
<FastRunMenu />
<Routes>
<Route index element={<Navigate to={'null/'} />} />
<Route path={':idDeposit'}>
<Route index element={<Navigate to={'map'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={'map'} element={<Map />} />
<Route path={'statistics_adw'} element={<StatisticsADW />} />
</Route>
</Routes>
</DepositContext.Provider>
</RootPathContext.Provider>
)
})
export default withPermissions(Deposit, [])

View File

@ -10,7 +10,7 @@ import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import '@styles/index.css' import '@styles/index.css'
import '@styles/statistics.less' import '@styles/pages/statistics.less'
const { Text } = Typography const { Text } = Typography
const { Summary } = RawTable 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) const makeSectionSorter = (key, name) => (a, b) => (a?.[key]?.[name] ?? 0) - (b?.[key]?.[name] ?? 0)
export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [ 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'), 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'), sorter: makeSectionSorter(key, 'time'),
}), }),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100, { makeNumericColumn((<>V<sub>рейсовая</sub></>), key, speedRender ?? speedNumericRender, undefined, 100, {
sorter: makeSectionSorter(key, 'speed'), sorter: makeSectionSorter(key, 'speed'),
}), }),
]) ])
@ -37,7 +37,7 @@ export const defaultColumns = [
makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }), 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 summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2
const getWellData = async (wellsList) => { const getWellData = async (wellsList) => {

View File

@ -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 (
<div className={'avg-column'}>
<div className={'avg-fill'} style={{ width: `${fillW}%` }} />
<div className={'avg-value'}>
{numericRender(avg)}
</div>
</div>
)
}
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 (
<>
<Button
size={'large'}
disabled={selectedWellsKeys.length <= 0}
onClick={onParamButtonClick}
>
Заполнить режимы текущей скважины
</Button>
<Modal
title={'Заполнить режимы текущей скважины'}
centered
open={isParamsModalVisible}
onCancel={() => setIsParamsModalVisible(false)}
width={1700}
footer={(
<Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}>
<Button
size={'large'}
disabled={params.length <= 0}
>Сохранить</Button>
</Popconfirm>
)}
>
<LoaderPortal show={showParamsLoader}>
<Table
bordered
size={'small'}
columns={paramsColumns}
dataSource={params}
pagination={false}
/>
</LoaderPortal>
</Modal>
</>
)
})
export default NewParamsTable

View File

@ -1,12 +1,13 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react' import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
import { Button, Badge, Divider, Modal, Row, Col } from 'antd' import { Button, Badge, Divider, Modal } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import SuspenseFallback from '@components/SuspenseFallback'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table, makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFactOld, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api' import { WellCompositeService } from '@api'
import { import {
hasPermission, hasPermission,
@ -16,8 +17,6 @@ import {
getOperations getOperations
} from '@utils' } from '@utils'
import NewParamsTable from './NewParamsTable'
import SuspenseFallback from '@asb/components/SuspenseFallback'
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd')) const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable')) const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable'))
@ -146,15 +145,15 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link> (text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
), ),
makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-'), makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-', { width: 100 }),
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Глубина, м', 'sectionWellDepth', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Продолжительность, ч', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Продолжительность, ч', 'sectionBuildDays', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('МСП, м/ч', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('МСП, м/ч', 'sectionRateOfPenetration', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'sectionRouteSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumn('НПВ, ч', 'nonProductiveHours', filtersMinMax, makeFilterMinMaxFunction, null, '80px'), makeNumericColumn('НПВ, ч', 'nonProductiveHours', undefined, makeFilterMinMaxFunction, '80px', { filters: filtersMinMax }),
{ {
title: 'TVD', title: 'TVD',
render: (value) => ( render: (value) => (
@ -200,11 +199,11 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
dataSource={rows} dataSource={rows}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true, y: 620 }} scroll={{ x: true, y: '30vh' }}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
/> />
<Divider /> <Divider style={{ marginTop: 0 }} />
<Badge.Ribbon text={'комбинированная скважина'} color={'gray'}> <Badge.Ribbon text={'комбинированная скважина'} color={'gray'}>
<h3>Выбранные секции</h3> <h3>Выбранные секции</h3>
</Badge.Ribbon> </Badge.Ribbon>
@ -214,12 +213,9 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
rowSelection={rowSelection} rowSelection={rowSelection}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true }} scroll={{ x: true, y: '30vh' }}
pagination={false} pagination={false}
/> />
<Row justify={'end'} style={{ margin: '1rem 0' }}>
<Col><NewParamsTable selectedWellsKeys={selectedWellsKeys} /></Col>
</Row>
<Modal <Modal
title={'TVD'} title={'TVD'}

View File

@ -12,7 +12,7 @@ import { OperationStatService, WellCompositeService } from '@api'
import WellCompositeSections from './WellCompositeSections' import WellCompositeSections from './WellCompositeSections'
import '@styles/well_composite.less' import '@styles/pages/well_composite.less'
const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells')) const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells'))

View File

@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
), ),
}, },
makeDateColumn('Дата загрузки', 'uploadDate'), makeDateColumn('Дата загрузки', 'uploadDate'),
makeNumericColumn('Размер', 'size', null, null, (value) => formatBytes(value)), makeNumericColumn('Размер', 'size', (value) => formatBytes(value)),
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }), makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }), makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
...(customColumns ?? []) ...(customColumns ?? [])

View File

@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const catSelectorRules = [{ const catSelectorRules = [{
required: true, required: true,

View File

@ -12,7 +12,7 @@ import { FileService } from '@api'
import MarksCard from './MarksCard' import MarksCard from './MarksCard'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
const { Search } = Input const { Search } = Input

View File

@ -18,7 +18,7 @@ import { formatDate, MimeTypes } from '@utils'
import MarksCard from './MarksCard' import MarksCard from './MarksCard'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const CommentPrompt = memo(({ isRequired = true, ...props }) => ( const CommentPrompt = memo(({ isRequired = true, ...props }) => (
<Poprompt <Poprompt

View File

@ -21,7 +21,7 @@ import CategoryRender from './CategoryRender'
import CategoryEditor from './CategoryEditor' import CategoryEditor from './CategoryEditor'
import CategoryHistory from './CategoryHistory' import CategoryHistory from './CategoryHistory'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const ID_STATE = { const ID_STATE = {
NotInitialized: 0, NotInitialized: 0,

View File

@ -18,7 +18,7 @@ import { MeasureService } from '@api'
import { View } from './View' import { View } from './View'
import '@styles/index.css' import '@styles/index.css'
import '@styles/measure.css' import '@styles/pages/measure.css'
const createEditingColumns = (cols, renderDelegate) => const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => col.map(col => ({ render: renderDelegate, ...col }))) cols.map(col => col.map(col => ({ render: renderDelegate, ...col })))

View File

@ -4,7 +4,7 @@ import { Empty, Form } from 'antd'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import '@styles/index.css' import '@styles/index.css'
import '@styles/measure.css' import '@styles/pages/measure.css'
export const View = memo(({ columns, item }) => !item || !columns?.length ? ( export const View = memo(({ columns, item }) => !item || !columns?.length ? (
<Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} />

View File

@ -2,7 +2,7 @@ import { Input } from 'antd'
import { RegExpIsFloat } from '@components/Table' import { RegExpIsFloat } from '@components/Table'
import '@styles/measure.css' import '@styles/pages/measure.css'
export const v = (text) => ( export const v = (text) => (
<div className={'v-div'}> <div className={'v-div'}>

View File

@ -22,7 +22,7 @@ import {
import AddGroupWindow from './AddGroupWindow' import AddGroupWindow from './AddGroupWindow'
import AddWidgetWindow, { makeWidgetFromWits } from './AddWidgetWindow' import AddWidgetWindow, { makeWidgetFromWits } from './AddWidgetWindow'
import '@styles/dashboard_nnb.less' import '@styles/pages/dashboard_nnb.less'
const getWitsInfo = async () => { const getWitsInfo = async () => {
// TODO: Добавить expire с принудительным обновлением // TODO: Добавить expire с принудительным обновлением

View File

@ -11,8 +11,8 @@ import { makeColumn, makeDateColumn, makeNumericColumn, makeNumericSorter, makeT
import { withPermissions } from '@utils' import { withPermissions } from '@utils'
import { MessageService } from '@api' import { MessageService } from '@api'
import '@styles/filter.less' import '@styles/components/filter.less'
import '@styles/message.less' import '@styles/pages/message.less'
const pageSize = 26 const pageSize = 26
const { Search } = Input const { Search } = Input
@ -29,7 +29,7 @@ const categoryDictionary = {
// Конфигурация таблицы // Конфигурация таблицы
export const makeMessageColumns = (idWell) => [ export const makeMessageColumns = (idWell) => [
makeDateColumn('Дата', 'date', undefined, undefined, { width: '120px' }), makeDateColumn('Дата', 'date', undefined, undefined, { width: '120px' }),
makeNumericColumn('Глубина, м', 'wellDepth', null, null, (depth, item) => ( makeNumericColumn('Глубина, м', 'wellDepth', (depth, item) => (
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
style={{ color: 'inherit'}} style={{ color: 'inherit'}}
@ -40,7 +40,7 @@ export const makeMessageColumns = (idWell) => [
{depth.toFixed(2)} {depth.toFixed(2)}
</Link> </Link>
</Tooltip> </Tooltip>
), '7rem'), ), undefined, '7rem'),
makeColumn('Категория', 'categoryId', { makeColumn('Категория', 'categoryId', {
width: '8rem', width: '8rem',
render: (_, item) => categoryDictionary[item.categoryId].title, render: (_, item) => categoryDictionary[item.categoryId].title,

View File

@ -10,8 +10,8 @@ import { DateRangeWrapper, makeColumn, makeNumericColumn, makeNumericRender, mak
import { arrayOrDefault, range, withPermissions } from '@utils' import { arrayOrDefault, range, withPermissions } from '@utils'
import { SubsystemOperationTimeService } from '@api' import { SubsystemOperationTimeService } from '@api'
import '@styles/filter.less' import '@styles/components/filter.less'
import '@styles/operation_time.less' import '@styles/pages/operation_time.less'
const subsystemColors = [ const subsystemColors = [
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#1abc9c', '#16a085', '#2ecc71', '#27ae60',
@ -26,10 +26,10 @@ const tableColumns = [
<div className={'table_color'} style={{ backgroundColor }} /> <div className={'table_color'} style={{ backgroundColor }} />
)}), )}),
makeTextColumn('Подсистема', 'subsystemName'), makeTextColumn('Подсистема', 'subsystemName'),
makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2), 200), makeNumericColumn('Использование, %', 'kUsage', val => (+val * 100).toFixed(2), undefined, 200),
makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, undefined, 200), makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, 200),
makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, undefined, 200), makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, 200),
makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0), 200), makeNumericColumn('Кол-во запусков', 'operationCount', makeNumericRender(0), undefined, 200),
] ]
// Выбор доступен только до текущей даты // Выбор доступен только до текущей даты

View File

@ -5,14 +5,13 @@ import { EditableTable, makeTextColumn } from '@components/Table'
import { DrillerService } from '@api' import { DrillerService } from '@api'
const columnOptions = { const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }
const columns = [ const columns = [
makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, columnOptions), makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, columnOptions),
makeTextColumn('Имя', 'name', undefined, undefined, undefined, columnOptions), makeTextColumn('Имя', 'name', undefined, undefined, undefined, columnOptions),
makeTextColumn('Отчество', 'patronymic', undefined, undefined, undefined, { editable: true }), makeTextColumn('Отчество', 'patronymic'),
] ]
const rowClassName = (record) => record.has ? 'driller_list_active' : '' const rowClassName = (record) => record.has ? 'driller_list_active' : ''

View File

@ -14,7 +14,6 @@ import { arrayOrDefault } from '@utils'
import { ScheduleService } from '@api' import { ScheduleService } from '@api'
const columnOptions = { const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }

View File

@ -4,7 +4,7 @@ import { D3Chart } from '@components/d3'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import { formatDate, makeDisplayValue } from '@utils' import { formatDate, makeDisplayValue } from '@utils'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const displayNumber = makeDisplayValue({ fixed: 2 }) const displayNumber = makeDisplayValue({ fixed: 2 })

View File

@ -2,7 +2,7 @@ import { memo } from 'react'
import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table' import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const numericRender = makeNumericRender(2) const numericRender = makeNumericRender(2)
@ -24,11 +24,11 @@ const makeDrillerSorter = (key) => (a, b) => {
export const columns = [ export const columns = [
makeTextColumn('Бурильщик', 'driller', null, makeDrillerSorter('driller'), drillerRender, { width: 200 }), makeTextColumn('Бурильщик', 'driller', null, makeDrillerSorter('driller'), drillerRender, { width: 200 }),
makeNumericColumn('Кол-во операций', 'count', null, null, (value) => parseInt(value), 150), makeNumericColumn('Кол-во операций', 'count', (value) => parseInt(value), undefined, 150),
makeNumericColumn('Среднее по ключевому показателю', 'averageValue', null, null, numericRender, 150), makeNumericColumn('Среднее по ключевому показателю', 'averageValue', numericRender, undefined, 150),
makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', null, null, numericRender, 150), makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', numericRender, undefined, 150),
makeNumericColumn('Эффективность (%)', 'efficiency', null, null, numericRender, 150), makeNumericColumn('Эффективность (%)', 'efficiency', numericRender, undefined, 150),
makeNumericColumn('Коэффициент потерь', 'loss', null, null, numericRender, 100), makeNumericColumn('Коэффициент потерь', 'loss', numericRender, undefined, 100),
] ]
export const OperationsTable = memo(({ data, height, ...other }) => ( export const OperationsTable = memo(({ data, height, ...other }) => (

View File

@ -4,18 +4,17 @@ import { Button, Modal } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeGroupColumn, makeNumericColumn, makeNumericRender, makeSelectColumn } from '@components/Table' import { EditableTable, makeGroupColumn, makeNumericColumn, makeNumericRender, makeSelectColumn } from '@components/Table'
import { DetectedOperationService, OperationValueService } from '@api' import { OperationValueService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
const columnOptions = { const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }
const scroll = { y: '75vh', scrollToFirstRowOnChange: true } const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
const numericRender = makeNumericRender(2) const numericRender = makeNumericRender(2)
export const TargetEditor = memo(({ loading, onChange }) => { export const TargetEditor = memo(({ loading, onChange, options }) => {
const [targets, setTargets] = useState([]) const [targets, setTargets] = useState([])
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -63,20 +62,17 @@ export const TargetEditor = memo(({ loading, onChange }) => {
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const categories = arrayOrDefault(await DetectedOperationService.getCategories())
const options = categories.map(({ id, name }) => ({ value: id, label: name }))
setTargetColumns([ setTargetColumns([
makeSelectColumn('Название', 'idOperationCategory', options, undefined, { ...columnOptions, width: 200 }, { makeSelectColumn('Название', 'idOperationCategory', options, undefined, { ...columnOptions, width: 200 }, {
showSearch: true, showSearch: true,
filterOption: (input, option) => filterOption: (input, option) =>
String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0 String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0
}), }),
makeNumericColumn('Цель', 'targetValue', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Цель', 'targetValue', numericRender, undefined, 150, columnOptions),
makeNumericColumn('Норм.', 'standardValue', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Норм.', 'standardValue', numericRender, undefined, 150, columnOptions),
makeGroupColumn('Глубина, м', [ makeGroupColumn('Глубина, м', [
makeNumericColumn('Начало', 'depthStart', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Начало', 'depthStart', numericRender, undefined, 150, columnOptions),
makeNumericColumn('Окончание', 'depthEnd', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Окончание', 'depthEnd', numericRender, undefined, 150, columnOptions),
]), ]),
]) ])
}, },
@ -84,7 +80,7 @@ export const TargetEditor = memo(({ loading, onChange }) => {
`Не удалось получить список категорий целей`, `Не удалось получить список категорий целей`,
{ actionName: 'Получение списка категорий целей', well } { actionName: 'Получение списка категорий целей', well }
) )
}, [well]) }, [options])
useEffect(() => { useEffect(() => {
updateTable() updateTable()

View File

@ -7,8 +7,8 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { unique } from '@utils/filters' import { unique } from '@utils/filters'
import { getPermissions, arrayOrDefault, range, withPermissions, pretify } from '@utils' import { getPermissions, arrayOrDefault, range, withPermissions, prettify } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService, WellOperationService } from '@api'
import DrillerList from './DrillerList' import DrillerList from './DrillerList'
import TargetEditor from './TargetEditor' import TargetEditor from './TargetEditor'
@ -16,7 +16,7 @@ import DrillerSchedule from './DrillerSchedule'
import OperationsChart from './OperationsChart' import OperationsChart from './OperationsChart'
import OperationsTable from './OperationsTable' import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const Operations = memo(() => { const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -26,7 +26,7 @@ const Operations = memo(() => {
const [data, setData] = useState({}) const [data, setData] = useState({})
const [drillers, setDrillers] = useState([]) const [drillers, setDrillers] = useState([])
const [drillersLoader, setDrillersLoader] = useState(false) const [drillersLoader, setDrillersLoader] = useState(false)
const [selectedCategory, setSelectedCategory] = useState(14) const [selectedCategory, setSelectedCategory] = useState(5011)
const [categories, setCategories] = useState() const [categories, setCategories] = useState()
const [well] = useWell() const [well] = useWell()
@ -67,7 +67,7 @@ const Operations = memo(() => {
const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0)) const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0))
const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique) const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique)
const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2 const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2
setYDomain(pretify(Math.max(maxTarget, value))) setYDomain(prettify(Math.max(maxTarget, value)))
}, [data]) }, [data])
useEffect(() => { useEffect(() => {
@ -78,7 +78,7 @@ const Operations = memo(() => {
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const categories = arrayOrDefault(await DetectedOperationService.getCategories()) const categories = arrayOrDefault(await WellOperationService.getCategories(well.id))
setCategories(categories.map((category) => ({ setCategories(categories.map((category) => ({
...category, ...category,
value: category.id, value: category.id,
@ -86,10 +86,10 @@ const Operations = memo(() => {
}))) })))
}, },
setIsLoading, setIsLoading,
'Не удалось загрзуить категории операций', 'Не удалось загрузить категории операций',
{ actionName: 'Получение категорий операций' } { actionName: 'Получение категорий операций' }
) )
}, []) }, [well])
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -135,6 +135,7 @@ const Operations = memo(() => {
onChange={setYDomain} onChange={setYDomain}
addonAfter={'мин'} addonAfter={'мин'}
addonBefore={'Верхняя граница'} addonBefore={'Верхняя граница'}
style={{width: '20em'}}
/> />
{permissions.driller.get && ( {permissions.driller.get && (
<> <>
@ -142,8 +143,8 @@ const Operations = memo(() => {
<DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} /> <DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} />
</> </>
)} )}
{permissions.detectedOperation.get && permissions.operationValue.get && ( {permissions.detectedOperation.get && permissions.operationValue.get && categories && (
<TargetEditor onChange={updateData} /> <TargetEditor onChange={updateData} options={categories} />
)} )}
</div> </div>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>

View File

@ -9,7 +9,7 @@ import { MessageService } from '@api'
import { makeMessageColumns } from '../Messages' import { makeMessageColumns } from '../Messages'
import '@styles/message.less' import '@styles/pages/message.less'
export const ActiveMessagesOnline = memo(({ well: givenWell }) => { export const ActiveMessagesOnline = memo(({ well: givenWell }) => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])

View File

@ -1,11 +1,11 @@
import { memo, useCallback, useMemo, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Select, Modal, Input, InputNumber } from 'antd' import { Modal, Input } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeNumericRender, EditableTable } from '@components/Table' import { makeNumericRender, EditableTable, makeSelectColumn, makeNumericColumn } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { SetpointsService } from '@api' import { SetpointsService } from '@api'
@ -19,24 +19,8 @@ export const SetpointSender = memo(({ onClose, visible, setpointNames }) => {
const [well] = useWell() const [well] = useWell()
const addingColumns = useMemo(() => [ const addingColumns = useMemo(() => [
{ makeSelectColumn('Наименование уставки', 'name', setpointNames, undefined, { width: 200, isRequired: true }),
title: 'Наименование уставки', makeNumericColumn('Значение', 'value', makeNumericRender(1), undefined, 125, { isRequired: true, align: 'right' }),
dataIndex: 'name',
editable: true,
isRequired: true,
width: 200,
input: <Select options={setpointNames} />,
render: (val) => setpointNames.find((name) => name.value === val)?.label
}, {
title: 'Значение',
dataIndex: 'value',
editable: true,
isRequired: true,
width: 125,
input: <InputNumber style={{ width: '100%' }} formatter={value => `${value}`.replace(',', '.')}/>,
render: makeNumericRender(1),
align: 'right'
}
], [setpointNames]) ], [setpointNames])
const onAdd = useCallback(async (sp) => setSetpoints((prevSp) => { const onAdd = useCallback(async (sp) => setSetpoints((prevSp) => {

View File

@ -5,9 +5,9 @@ import { Grid, GridItem } from '@components/Grid'
import { getChartIcon, makeDisplayValue } from '@utils' import { getChartIcon, makeDisplayValue } from '@utils'
import moment from 'moment' import moment from 'moment'
const defaultFormater = makeDisplayValue({ def: '---', fixed: 2 }) const defaultFormatter = makeDisplayValue({ def: '---', fixed: 2 })
const defaultValueRender = (v, unit) => ( const defaultValueRender = (v, unit) => (
<>{defaultFormater(v)} {unit ?? ''}</> <>{defaultFormatter(v)} {unit ?? ''}</>
) )
export const cursorRender = (group, data, flowData) => { export const cursorRender = (group, data, flowData) => {
@ -17,7 +17,7 @@ export const cursorRender = (group, data, flowData) => {
const firstChart = group.charts[0] const firstChart = group.charts[0]
const y = firstChart.y(d) const y = firstChart.y(d)
const yDate = moment(y) const yDate = moment(y)
const flow = flowData.filter((row) => yDate.isBetween(row.dateStart, row.dateEnd, 's', '[]')) const flow = flowData?.filter((row) => yDate.isBetween(row.dateStart, row.dateEnd, 's', '[]'))
const yValue = firstChart.yAxis.format?.(y) ?? defaultValueRender(y, firstChart.yAxis.unit) const yValue = firstChart.yAxis.format?.(y) ?? defaultValueRender(y, firstChart.yAxis.unit)
const xFormat = (chart) => { const xFormat = (chart) => {
const v = chart.x(d) const v = chart.x(d)
@ -42,13 +42,13 @@ export const cursorRender = (group, data, flowData) => {
{group.charts.filter((chart) => chart.type === 'rect_area').map((chart, i) => { {group.charts.filter((chart) => chart.type === 'rect_area').map((chart, i) => {
const minX = getByAccessor(chart.minXAccessor) const minX = getByAccessor(chart.minXAccessor)
const maxX = getByAccessor(chart.maxXAccessor) const maxX = getByAccessor(chart.maxXAccessor)
const value = (row) => <span>{defaultFormater(minX(row))} - {defaultFormater(maxX(row))}</span> const value = (row) => <span>{defaultFormatter(minX(row))} - {defaultFormatter(maxX(row))}</span>
return ( return (
<Fragment key={chart.key}> <Fragment key={chart.key}>
<GridItem row={i+2} col={1} style={{ padding: '2px 0' }}>{getChartIcon(chart)}</GridItem> <GridItem row={i+2} col={1} style={{ padding: '2px 0' }}>{getChartIcon(chart)}</GridItem>
<GridItem row={i+2} col={2}>{chart.shortLabel || chart.label}</GridItem> <GridItem row={i+2} col={2}>{chart.shortLabel || chart.label}</GridItem>
{flow.map((row, j) => ( {flow?.map((row, j) => (
<GridItem key={`${j}`} row={i+2+(j++)} col={3} style={{ paddingRight: 0, textAlign: 'end' }}> <GridItem key={`${j}`} row={i+2+(j++)} col={3} style={{ paddingRight: 0, textAlign: 'end' }}>
{chart.xAxis.format?.(row) ?? value(row)} {chart.xAxis.format?.(row) ?? value(row)}
</GridItem> </GridItem>

View File

@ -13,13 +13,12 @@ import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { formatDate, hasPermission, isRawDate, withPermissions } from '@utils' import { formatDate, hasPermission, isRawDate, withPermissions } from '@utils'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
import { import {
DrillFlowChartService,
OperationStatService, OperationStatService,
TelemetryDataSaubService, TelemetryDataSaubService,
TelemetryDataSpinService TelemetryDataSpinService
} from '@api' } from '@api'
import { calcFlowData, makeChartGroups, yAxis } from './dataset' import { makeChartGroups, yAxis } from './dataset'
import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods' import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
import LimitingParameterStatistics from './LimitingParameterStatistics' import LimitingParameterStatistics from './LimitingParameterStatistics'
import ActiveMessagesOnline from './ActiveMessagesOnline' import ActiveMessagesOnline from './ActiveMessagesOnline'
@ -33,8 +32,8 @@ import MomentStabPicDisabled from '@images/DempherOff.png'
import SpinPicEnabled from '@images/SpinEnabled.png' import SpinPicEnabled from '@images/SpinEnabled.png'
import SpinPicDisabled from '@images/SpinDisabled.png' import SpinPicDisabled from '@images/SpinDisabled.png'
import '@styles/telemetry_view.less' import '@styles/pages/telemetry_view.less'
import '@styles/message.less' import '@styles/pages/message.less'
const { Option } = Select const { Option } = Select
@ -65,12 +64,12 @@ export const normalizeData = (data) => data?.map(item => ({
const dateSorter = makeDateSorter('date') const dateSorter = makeDateSorter('date')
const defaultDate = () => new Date(Date.now() - defaultPeriod * 1000) const defaultDate = () => new Date(Date.now() - defaultPeriod * 1000)
const makeSubjectSubsription = (subject$, handler) => { const makeSubjectSubscription = (subject$, handler) => {
const subscribtion = subject$.pipe( const subscription = subject$.pipe(
buffer(subject$.pipe(throttleTime(700))) buffer(subject$.pipe(throttleTime(700)))
).subscribe((data) => handler(data.flat().filter(Boolean))) ).subscribe((data) => handler(data.flat().filter(Boolean)))
return () => subscribtion.unsubscribe() return () => subscription.unsubscribe()
} }
const getRowDate = (row) => row && isRawDate(row.date) ? new Date(row.date) : null const getRowDate = (row) => row && isRawDate(row.date) ? new Date(row.date) : null
@ -82,7 +81,6 @@ const TelemetryView = memo(() => {
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([]) const [dataSpin, setDataSpin] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [flowChartData, setFlowChartData] = useState([])
const [rop, setRop] = useState(null) const [rop, setRop] = useState(null)
const [chartMethods, setChartMethods] = useState() const [chartMethods, setChartMethods] = useState()
@ -148,7 +146,6 @@ const TelemetryView = memo(() => {
const spinSubject$ = useMemo(() => new BehaviorSubject(), []) const spinSubject$ = useMemo(() => new BehaviorSubject(), [])
const filteredData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain]) const filteredData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain])
const flowData = useMemo(() => calcFlowData(dataSaub, flowChartData), [dataSaub, flowChartData])
const chartGroups = useMemo(() => makeChartGroups(), []) const chartGroups = useMemo(() => makeChartGroups(), [])
useEffect(() => { useEffect(() => {
@ -171,8 +168,8 @@ const TelemetryView = memo(() => {
setSearchParams(params) setSearchParams(params)
}, [archiveMode, endDate, chartInterval]) }, [archiveMode, endDate, chartInterval])
useEffect(() => makeSubjectSubsription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub]) useEffect(() => makeSubjectSubscription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub])
useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) useEffect(() => makeSubjectSubscription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin])
useEffect(() => { useEffect(() => {
if (archiveMode) return if (archiveMode) return
@ -234,8 +231,6 @@ const TelemetryView = memo(() => {
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const flowChart = await DrillFlowChartService.getByIdWell(well.id)
setFlowChartData(flowChart ?? [])
const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) const rop = await OperationStatService.getClusterRopStatByIdWell(well.id)
setRop(rop) setRop(rop)
let dates = await TelemetryDataSaubService.getDataDatesRange(well.id) let dates = await TelemetryDataSaubService.getDataDatesRange(well.id)
@ -314,7 +309,6 @@ const TelemetryView = memo(() => {
{...chartProps} {...chartProps}
yDomain={domain} yDomain={domain}
data={filteredData} data={filteredData}
flowData={flowData}
methods={setChartMethods} methods={setChartMethods}
datasetGroups={chartGroups} datasetGroups={chartGroups}
onWheel={onWheel} onWheel={onWheel}

View File

@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { WellFinalDocumentsService } from '@api' import { WellFinalDocumentsService } from '@api'
import { formatDate } from '@utils' import { formatDate } from '@utils'
import '@styles/well_case.less' import '@styles/pages/well_case.less'
export const HistoryTable = memo(({ category }) => { export const HistoryTable = memo(({ category }) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)

View File

@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { WellFinalDocumentsService } from '@api' import { WellFinalDocumentsService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import '@styles/well_case.less' import '@styles/pages/well_case.less'
const filterCategoriesByText = (text) => (cat) => const filterCategoriesByText = (text) => (cat) =>
!text || !!cat?.nameCategory?.toLowerCase().includes(text.toLowerCase()) !text || !!cat?.nameCategory?.toLowerCase().includes(text.toLowerCase())

View File

@ -15,7 +15,7 @@ import { withPermissions } from '@utils'
import WellCaseEditor from './WellCaseEditor' import WellCaseEditor from './WellCaseEditor'
import { HistoryTable } from './HistoryTable' import { HistoryTable } from './HistoryTable'
import '@styles/well_case.less' import '@styles/pages/well_case.less'
const expandable = { const expandable = {
expandedRowRender: (category) => <HistoryTable category={category} />, expandedRowRender: (category) => <HistoryTable category={category} />,

View File

@ -13,7 +13,7 @@ import {
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' import { makeItem, PrivateMenu } from '@components/PrivateMenu'
export const menuItems = [ export const menuItems = [
makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [ makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [
@ -40,7 +40,6 @@ export const menuItems = [
makeItem('План', 'plan', [], <TableOutlined />), makeItem('План', 'plan', [], <TableOutlined />),
makeItem('Факт', 'fact', [], <TableOutlined />), makeItem('Факт', 'fact', [], <TableOutlined />),
makeItem('РТК', 'drillProcessFlow', [], <BarChartOutlined />), makeItem('РТК', 'drillProcessFlow', [], <BarChartOutlined />),
makeItem('Режимы', 'params', [], <ControlOutlined />),
]), ]),
makeItem('Документы', 'document', [], <FolderOutlined />, [ makeItem('Документы', 'document', [], <FolderOutlined />, [
makeItem('Растворный сервис', 'fluidService', [], <FolderOutlined />), makeItem('Растворный сервис', 'fluidService', [], <FolderOutlined />),
@ -59,15 +58,15 @@ export const menuItems = [
makeItem('Дело скважины', 'well_case', [], <FolderOutlined />), makeItem('Дело скважины', 'well_case', [], <FolderOutlined />),
] ]
export const NavigationMenu = memo((props) => ( export const WellNavigationMenu = memo((props) => (
<PrivateWellMenu <PrivateMenu
{...props} {...props}
items={menuItems} items={menuItems}
rootPath={'/well/{wellId}'} rootPath={'/well/{idWell}'}
mode={'inline'} mode={'inline'}
theme={'dark'} theme={'dark'}
style={{ backgroundColor: 'transparent' }} style={{ backgroundColor: 'transparent' }}
/> />
)) ))
export default NavigationMenu export default WellNavigationMenu

View File

@ -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 LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync, download } from '@components/factory'
import { EditableTable, makeNumericMinMax, makeNumericStartEnd } from '@components/Table' import { ProcessMapService, WellOperationService } from '@api'
import { DrillFlowChartService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
const columns = [ const style = { margin: 4 }
makeNumericStartEnd('Глубина, м', 'depth'),
makeNumericMinMax('Нагрузка, т', 'axialLoad'), const ImportExportBar = memo(({ well: givenWell, disabled }) => {
makeNumericMinMax('Перепад давления, атм', 'pressure'), const [wellContext] = useWell()
makeNumericMinMax('Момент на ВСП, кН·м', 'rotorTorque'), const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext])
makeNumericMinMax('Обороты на ВСП, об/мин', 'rotorSpeed'),
makeNumericMinMax('Расход, л/с', 'flow') const downloadExport = useCallback(
] async () => await download(`/api/ProcessMap/getReportFile/${well.id}`),
[well.id]
)
return (
<div>
<Tooltip title={'Выгрузка расширенной автоформируемой РТК'}>
<Button disabled={disabled} icon={<FileOutlined />} style={style} onClick={downloadExport} />
</Tooltip>
</div>
)
})
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(() => { export const DrillProcessFlow = memo(() => {
const [flows, setFlows] = useState([]) const [flows, setFlows] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([])
const [well] = useWell() const [well] = useWell()
const setTopRightBlock = useTopRightBlock()
const updateFlows = useCallback(() => invokeWebApiWrapperAsync( const updateFlows = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
const flows = await DrillFlowChartService.getByIdWell(well.id) const flows = await ProcessMapService.getByIdWell(well.id)
setFlows(arrayOrDefault(flows)) setFlows(arrayOrDefault(flows))
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить режимно-технологическую карту`, `Не удалось загрузить режимно-технологическую карту`,
{ actionName: 'Получение режимно-технологической карты', well } { actionName: 'Получение режимно-технологической карты', well },
), [well]) ), [well])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const columns = await getColumns(well.id)
setColumns(columns)
},
setShowLoader,
`Не удалось загрузить список конструкций секций`,
{ actionName: 'Получение списка конструкций секций', well },
)
}, [well])
useEffect(() => { useEffect(() => {
updateFlows() updateFlows()
}, [well]) }, [well])
useEffect(() => setTopRightBlock((well) => (
<ImportExportBar well={well} />
)), [setTopRightBlock])
const tableHandlers = useMemo(() => { const tableHandlers = useMemo(() => {
const handlerProps = { const handlerProps = {
service: DrillFlowChartService, service: ProcessMapService,
setLoader: setShowLoader, setLoader: setShowLoader,
onComplete: updateFlows, onComplete: updateFlows,
permission: 'DrillFlowChart.edit', permission: 'DrillFlowChart.edit',
@ -49,7 +115,12 @@ export const DrillProcessFlow = memo(() => {
return { return {
add: { ...handlerProps, action: 'insert', actionName: 'Добавление месторождения', recordParser }, add: { ...handlerProps, action: 'insert', actionName: 'Добавление месторождения', recordParser },
edit: { ...handlerProps, action: 'update', 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]) }, [updateFlows, well.id])

View File

@ -31,26 +31,20 @@ const dayWithoutNptRender = (_, row) => dayRender((row.day ?? 0) - (row.nptHours
const generateColumns = (showNpt = false, categories = [], sectionTypes = []) => [ const generateColumns = (showNpt = false, categories = [], sectionTypes = []) => [
makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, {
sorter: makeNumericSorter('idWellSectionType'), sorter: makeNumericSorter('idWellSectionType'),
editable: true,
width: 160, width: 160,
}), }),
makeSelectColumn('Операция', 'idCategory', categories, undefined, { makeSelectColumn('Операция', 'idCategory', categories, undefined, {
sorter: makeNumericSorter('idCategory'), sorter: makeNumericSorter('idCategory'),
editable: true,
width: 200, width: 200,
}), }),
makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null, { editable: true, width: 300, input: <TextArea/> }), makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null, { width: 300, input: <TextArea/> }),
makeColumn('Глубина забоя на начало, м', 'depthStart', makeNumericColumnOptions(2, 'depthStart')), makeNumericColumn('Глубина забоя на начало, м', 'depthStart'),
makeColumn('Глубина забоя при завершении, м', 'depthEnd', makeNumericColumnOptions(2, 'depthEnd')), makeNumericColumn('Глубина забоя при завершении, м', 'depthEnd'),
makeDateColumn('Время начала', 'dateStart', undefined, undefined, { makeDateColumn('Время начала', 'dateStart', undefined, undefined, { width: 170, initialValue: moment().format() }),
editable: true, makeNumericColumn('День', 'day', dayRender, undefined, 80),
width: 170, showNpt && makeNumericColumn('День без НПВ', 'dayWithoutNpt', dayWithoutNptRender, undefined, 80),
initialValue: moment().format(), makeNumericColumn('Часы', 'durationHours', undefined, undefined, 70),
}), makeTextColumn('Комментарий', 'comment', null, null, null, { input: <TextArea /> }),
makeNumericColumn('День', 'day', null, null, dayRender, 80),
showNpt && makeNumericColumn('День без НПВ', 'dayWithoutNpt', null, null, dayWithoutNptRender, 80),
makeColumn('Часы', 'durationHours', { ...makeNumericColumnOptions(2, 'durationHours'), width: 70 }),
makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }),
].filter(Boolean) ].filter(Boolean)
export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => { export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {

View File

@ -5,7 +5,7 @@ import { makeNumericRender } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils' import { formatDate, fractionalSum } from '@utils'
import '@styles/tvd.less' import '@styles/pages/tvd.less'
const { Item } = Descriptions const { Item } = Descriptions

View File

@ -4,15 +4,15 @@ import { FilterOutlined } from '@ant-design/icons'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeDateColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' import { makeDateColumn, makeNumericColumn, makeTextColumn, Table } from '@components/Table'
import '@styles/tvd.less' import '@styles/pages/tvd.less'
export const columns = [ export const columns = [
makeTextColumn('Конструкция секции', 'wellSectionTypeName', null, null, null, { width: 140 }), makeTextColumn('Конструкция секции', 'wellSectionTypeName', null, null, null, { width: 140 }),
makeNumericColumn('Глубина', 'depth', null, null, null, 80), makeNumericColumn('Глубина', 'depth', undefined, undefined, 80),
makeDateColumn('Дата начала', 'date', false, undefined, { width: 90 }), makeDateColumn('Дата начала', 'date', false, undefined, { width: 90 }),
makeNumericColumn('Длительность (ч)', 'durationHours', null, null, makeNumericRender(2), 120), makeNumericColumn('Длительность (ч)', 'durationHours', undefined, undefined, 120),
makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null), makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null),
makeTextColumn('Комментарий', 'comment'), makeTextColumn('Комментарий', 'comment'),
] ]

Some files were not shown because too many files have changed in this diff Show More