* блок utils подразделён на functions, hooks, types и filters

* добавлен хук useFunctionalValue
* добавлен хук useCachedFetch
* удалён RCA
* добавлен конфиг babel
* добавлен конфиг webpack
* обновлены все пакеты
* добавлены базовые моки
* добавлены конфиги для тестов
* добавлена кнопка копирования url
* роутер переписан
* в Messages добавлен переход в Архив при клике на сообщение
This commit is contained in:
goodmice 2022-06-09 17:51:41 +05:00
parent b97066af6e
commit d5e827532d
125 changed files with 9242 additions and 28633 deletions

1
__mocks__/fileMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = 'test-file-stub'

1
__mocks__/styleMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = {}

7
babel.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {runtime: 'automatic'}],
'@babel/preset-typescript',
],
}

View File

@ -1,25 +0,0 @@
const CracoLessPlugin = require('craco-less')
const CracoAlias = require('craco-alias')
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
//modifyVars: { '@primary-color': '#E20000' },
javascriptEnabled: true,
},
},
},
}, {
plugin: CracoAlias,
options: {
source: 'tsconfig',
baseUrl: './src',
tsConfigPath: './tsconfig.paths.json'
}
}
],
}

30
custom.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react'
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>
const src: string
export default src
}

33712
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,41 +3,36 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@craco/craco": "^6.1.2", "@microsoft/signalr": "^6.0.5",
"@microsoft/signalr": "^6.0.4", "@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^11.2.6", "@testing-library/user-event": "^14.2.0",
"@testing-library/user-event": "^12.8.3", "@types/react-dom": "^18.0.5",
"@types/react-dom": "^18.0.3", "antd": "^4.20.7",
"antd": "^4.15.0", "chart.js": "^3.8.0",
"chart.js": "^3.6.0",
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"chartjs-plugin-datalabels": "^2.0.0-rc.1", "chartjs-plugin-datalabels": "^2.0.0",
"chartjs-plugin-zoom": "^1.1.1", "chartjs-plugin-zoom": "^1.2.1",
"craco-less": "^1.17.1",
"d3": "^7.4.4", "d3": "^7.4.4",
"moment": "^2.29.1", "less": "^4.1.2",
"pigeon-maps": "^0.19.7", "less-loader": "^11.0.0",
"react": "^17.0.2", "moment": "^2.29.3",
"react-dom": "^17.0.2", "pigeon-maps": "^0.21.0",
"react-router-dom": "^5.2.0", "react": "^18.1.0",
"react-scripts": "4.0.3", "react-dom": "^18.1.0",
"rxjs": "^7.5.4", "react-router-dom": "^6.3.0",
"typescript": "^4.2.3", "rxjs": "^7.5.5",
"web-vitals": "^1.1.1" "typescript": "^4.7.2",
"web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "webpack-dev-server --mode=development --open --hot",
"build": "craco build", "build": "webpack --mode=production",
"test": "craco test", "test": "jest",
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api", "oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api", "oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
"oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api", "oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api",
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api", "oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
"react_start": "react-scripts start",
"react_build": "react-scripts build",
"react_test": "react-scripts test",
"eject": "react-scripts eject"
}, },
"proxy": "http://46.146.209.148:89", "proxy": "http://46.146.209.148:89",
"eslintConfig": { "eslintConfig": {
@ -58,12 +53,57 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"jest": {
"moduleFileExtensions": [
"js",
"jsx",
"ts",
"tsx"
],
"moduleDirectories": [
"node_modules",
"src"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"^@asb(.*)$": "<rootDir>/src$1",
"^@api(.*)$": "<rootDir>/src/services/api$1",
"^@components(.*)$": "<rootDir>/src/components$1",
"^@services(.*)$": "<rootDir>/src/services$1",
"^@pages(.*)$": "<rootDir>/src/pages$1",
"^@utils(.*)$": "<rootDir>/src/utils$1",
"^@images(.*)$": "<rootDir>/src/images$1",
"^@styles(.*)$": "<rootDir>/src/styles$1"
}
},
"devDependencies": { "devDependencies": {
"@types/d3": "^7.1.0", "@babel/core": "^7.18.2",
"@types/react": "^17.0.3", "@babel/preset-env": "^7.18.2",
"@types/react-router-dom": "^5.3.2", "@babel/preset-react": "^7.17.12",
"craco-alias": "^3.0.1", "@babel/preset-typescript": "^7.17.12",
"openapi-typescript": "^3.4.1", "@testing-library/react": "^13.3.0",
"openapi-typescript-codegen": "^0.21.0" "@types/d3": "^7.4.0",
"@types/jest": "^28.1.0",
"@types/react": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"babel-jest": "^28.1.0",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"interpolate-html-plugin": "^4.0.0",
"jest": "^28.1.0",
"openapi-typescript": "^5.4.0",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"react-test-renderer": "^18.1.0",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.0",
"url-loader": "^4.1.1",
"webpack": "^5.73.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.1"
} }
} }

View File

@ -2,16 +2,12 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="white" /> <meta name="theme-color" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" /> <meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" /> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
<meta <meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
name="description"
content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>АСБ Vision</title> <title>АСБ Vision</title>
</head> </head>
<body> <body>

View File

@ -1,18 +1,17 @@
import { import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
BrowserRouter as Router,
Switch,
Route
} from 'react-router-dom'
import { memo } from 'react' import { memo } from 'react'
import { ConfigProvider } from 'antd' import { ConfigProvider } from 'antd'
import locale from 'antd/lib/locale/ru_RU' import locale from 'antd/lib/locale/ru_RU'
import { PrivateRoute } from '@components/Private' import { RootPathContext } from '@asb/context'
import { getUserToken } from '@utils/storage' import { getUserToken, NoAccessComponent } from '@utils'
import { OpenAPI } from '@api' import { OpenAPI } from '@api'
import Main from '@pages/Main' import AdminPanel from '@pages/AdminPanel'
import Well from '@pages/Well'
import Login from '@pages/Login' import Login from '@pages/Login'
import Cluster from '@pages/Cluster'
import Deposit from '@pages/Deposit'
import Register from '@pages/Register' import Register from '@pages/Register'
import '@styles/App.less' import '@styles/App.less'
@ -23,19 +22,26 @@ OpenAPI.HEADERS = {'Content-Type': 'application/json'}
export const App = memo(() => ( export const App = memo(() => (
<ConfigProvider locale={locale}> <ConfigProvider locale={locale}>
<Router> <RootPathContext.Provider value={''}>
<Switch> <Router>
<Route path={'/login'}> <Routes>
<Login /> <Route index element={<Navigate to={Deposit.getKey()} replace />} />
</Route> <Route path={'*'} element={<NoAccessComponent />} />
<Route path={'/register'}>
<Register /> {/* Public pages */}
</Route> <Route path={Login.route} element={<Login />} />
<PrivateRoute path={'/'}> <Route path={Register.route} element={<Register />} />
<Main />
</PrivateRoute> {/* Admin pages */}
</Switch> <Route path={AdminPanel.route} element={<AdminPanel />} />
</Router>
{/* User pages */}
<Route path={Deposit.route} element={<Deposit />} />
<Route path={Cluster.route} element={<Cluster />} />
<Route path={Well.route} element={<Well />} />
</Routes>
</Router>
</RootPathContext.Provider>
</ConfigProvider> </ConfigProvider>
)) ))

View File

@ -3,7 +3,7 @@ import { Rule } from 'antd/lib/form'
import { Form, Input, Modal, FormProps } from 'antd' import { Form, Input, Modal, FormProps } from 'antd'
import { AuthService, UserDto } from '@api' import { AuthService, UserDto } from '@api'
import { getUserId, getUserLogin } from '@utils/storage' import { getUserId, getUserLogin } from '@utils'
import { passwordRules, createPasswordRules } from '@utils/validationRules' import { passwordRules, createPasswordRules } from '@utils/validationRules'
import LoaderPortal from './LoaderPortal' import LoaderPortal from './LoaderPortal'

View File

@ -0,0 +1,55 @@
import { cloneElement, memo, useCallback, useMemo, useState } from 'react'
import { Button, ButtonProps } from 'antd'
import { CopyOutlined } from '@ant-design/icons'
import { invokeWebApiWrapperAsync, notify } from './factory'
export type CopyUrlProps = {
sendLoading?: boolean
hideUnsupported?: boolean
onCopy?: () => (void | Promise<void>)
children: JSX.Element
}
export const CopyUrl = memo<CopyUrlProps>(({ children, onCopy, sendLoading, hideUnsupported = true }) => {
const props = useMemo(() => children.props, [children])
const [loading, setLoading] = useState(false)
const supported = !!navigator?.clipboard?.writeText // Проверка поддержки
const onClick = useCallback((event: MouseEvent) => {
if (supported) {
invokeWebApiWrapperAsync(
async () => {
await navigator.clipboard.writeText(window.location.href)
await onCopy?.()
notify('URL успешно скопирован', 'info')
},
setLoading,
`Не удалось скопировать URL в буфер обмена`
)
}
props.onClick?.(event) // Запуск onClick по-умолчанию
}, [props])
if (hideUnsupported && !supported) return null
return cloneElement(children, { onClick, loading: sendLoading ? loading : props.loading })
})
export type CopyUrlButtonProps = Omit<CopyUrlProps, 'children'> & ButtonProps
export const CopyUrlButton = memo<CopyUrlButtonProps>(({ sendLoading, hideUnsupported, onCopy, ...other }) => {
return (
<CopyUrl sendLoading={sendLoading} hideUnsupported={hideUnsupported} onCopy={onCopy}>
<Button
icon={<CopyOutlined />}
title={'Скопировать URL в буфер обмена'}
{...other}
/>
</CopyUrl>
)
})
export default CopyUrl

View File

@ -15,7 +15,7 @@ export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props
<Layout.Content> <Layout.Content>
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}> <PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}> <Button size={'large'}>
<Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link> <Link to={'/'}>Вернуться на сайт</Link>
</Button> </Button>
</PageHeader> </PageHeader>
<Layout> <Layout>

View File

@ -3,13 +3,14 @@ import { Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader' import PageHeader from '@components/PageHeader'
import WellTreeSelector from '@components/selectors/WellTreeSelector' import WellTreeSelector from '@components/selectors/WellTreeSelector'
import { wrapPrivateComponent } from '@utils'
export type LayoutPortalProps = LayoutProps & { export type LayoutPortalProps = LayoutProps & {
title?: ReactNode title?: ReactNode
noSheet?: boolean noSheet?: boolean
} }
export const LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props }) => ( const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props }) => (
<Layout.Content> <Layout.Content>
<PageHeader title={title}> <PageHeader title={title}>
<WellTreeSelector /> <WellTreeSelector />
@ -22,4 +23,8 @@ export const LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props
</Layout.Content> </Layout.Content>
)) ))
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
requirements: ['Deposit.get'],
})
export default LayoutPortal export default LayoutPortal

View File

@ -1,5 +1,5 @@
export { AdminLayoutPortal } from './AdminLayoutPortal' export * from './AdminLayoutPortal'
export { LayoutPortal } from './LayoutPortal' export * from './LayoutPortal'
export type { AdminLayoutPortalProps } from './AdminLayoutPortal' export type { AdminLayoutPortalProps } from './AdminLayoutPortal'
export type { LayoutPortalProps } from './LayoutPortal' export type { LayoutPortalProps } from './LayoutPortal'

View File

@ -20,7 +20,7 @@ export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Монит
return ( return (
<Layout> <Layout>
<Layout.Header className={'header'} {...other}> <Layout.Header className={'header'} {...other}>
<Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}> <Link to={'/'} style={{ height: headerHeight }}>
<Logo /> <Logo />
</Link> </Link>
{children} {children}

View File

@ -1,6 +1,6 @@
import { memo, ReactElement } from 'react' import { memo, ReactElement } from 'react'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils'
export type PrivateContentProps = { export type PrivateContentProps = {
absolutePath: string absolutePath: string

View File

@ -1,25 +1,19 @@
import { memo } from 'react' import { memo } from 'react'
import { Redirect, Route, RouteProps, useLocation } from 'react-router-dom' import { Navigate, Route, RouteProps } from 'react-router-dom'
import { getUserId } from '@utils/storage' import { isURLAvailable } from '@utils'
import { isURLAvailable } from '@utils/permissions'
import { getDefaultRedirectPath } from './PrivateRoutes'
export type PrivateDefaultRouteProps = RouteProps & { export type PrivateDefaultRouteProps = RouteProps & {
urls: string[] urls: string[]
elseRedirect?: string elseRedirect?: string
} }
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => { export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
const location = useLocation() <Route {...other} path={'/'} element={(
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
return ( )} />
<Route {...other} path={'/'}> ))
<Redirect to={{
pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login'),
state: { from: location.pathname },
}} />
</Route>
)
})
export default PrivateDefaultRoute export default PrivateDefaultRoute

View File

@ -1,47 +1,76 @@
import { join } from 'path' import { join } from 'path'
import { Menu, MenuItemProps, MenuProps } from 'antd' import { Menu, MenuProps } from 'antd'
import { Children, cloneElement, memo, ReactElement, useContext, useMemo } from 'react' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, useLocation } from 'react-router-dom' import { Link, LinkProps } from 'react-router-dom'
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
import { RootPathContext } from '@asb/context' import { useRootPath } from '@asb/context'
import { isURLAvailable } from '@utils/permissions' import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
export type PrivateMenuProps = MenuProps & { root?: string } export type PrivateMenuProps = MenuProps & { root?: string }
export type PrivateMenuLinkProps = MenuItemProps & { export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
tabName?: string icon?: ReactNode
danger?: boolean
title?: ReactNode
content?: PrivateComponent<any>
path?: string path?: string
title: string
visible?: boolean visible?: boolean
permissions?: string[]
} }
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ tabName = '', path = '', title, ...other }) => { export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, danger, icon, path = '', title, ...other }) => (
const location = useLocation() <Menu.Item icon={icon ?? content?.icon} danger={danger}>
return ( <Link to={path} {...other}>{title ?? content?.title}</Link>
<Menu.Item key={tabName} {...other}> </Menu.Item>
<Link to={{ pathname: path, state: { from: location.pathname }}}>{title}</Link> ))
</Menu.Item>
)
})
const PrivateMenuMain = memo<PrivateMenuProps>(({ root, children, ...other }) => { const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
const rootContext = useContext(RootPathContext) const rootContext = useRootPath()
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext]) const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const items = useMemo(() => Children.toArray(children).map((child) => { const tab = getTabname()
const element = child as ReactElement const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
let key = element.key?.toString()
const visible: boolean | undefined = element.props.visible
if (key && visible !== false) {
key = key.slice(key.lastIndexOf('$') + 1) // Ключ автоматический преобразуется в "(.+)\$ключ"
const path = join(rootPath, key)
if (visible || isURLAvailable(path))
return cloneElement(element, { key, path, tabName: key })
}
return null
}), [children, rootPath])
return <Menu children={items} {...other} /> const items = useMemo(() => Children.map(children, (child) => {
if (!child || !isValidElement<PrivateMenuLinkProps>(child))
return null
const content: PrivateProps | undefined = child.props.content
const visible: boolean | undefined = child.props.visible
if (visible === false) return null
let key
if (content?.key)
key = content.key
else if (content?.route)
key = content.route
else if (child.key) {
key = child.key?.toString()
key = key.slice(key.lastIndexOf('$') + 1)
} else return null
const permissions = child.props.permissions ?? content?.requirements
const path = child.props.path ?? join(rootPath, key)
if (visible || hasPermission(permissions))
return {
...child.props,
icon: null,
key,
label: <PrivateMenuLink {...child.props} path={path} />,
}
return null
})?.filter((v) => v) ?? [], [children, rootPath])
return (
<Menu
selectable={selectable ?? true}
mode={mode ?? 'horizontal'}
selectedKeys={keys}
items={items as ItemType[]}
{...other}
/>
)
}) })
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink }) export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })

View File

@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
import { memo, NamedExoticComponent } from 'react' import { memo, NamedExoticComponent } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils'
export type PrivateMenuItemProps = MenuItemProps & { export type PrivateMenuItemProps = MenuItemProps & {
root: string root: string
@ -20,7 +20,7 @@ export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '',
const location = useLocation() const location = useLocation()
return ( return (
<PrivateMenuItem key={path} root={root} path={path} {...other}> <PrivateMenuItem key={path} root={root} path={path} {...other}>
<Link to={{ pathname: join(root, path), state: { from: location.pathname }}}>{title}</Link> <Link to={join(root, path)}>{title}</Link>
</PrivateMenuItem> </PrivateMenuItem>
) )
}) })

View File

@ -1,31 +1,28 @@
import { Location } from 'history'
import { memo, ReactNode } from 'react'
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { memo, ReactNode } from 'react'
import { Navigate, Route, RouteProps } from 'react-router-dom'
import { getUserId } from '@utils/storage' import { getUserId, isURLAvailable } from '@utils'
import { isURLAvailable } from '@utils/permissions'
export type PrivateRouteProps = RouteProps & { export type PrivateRouteProps = RouteProps & {
root?: string root?: string
path: string path: string
children?: ReactNode children?: ReactNode
redirect?: (location?: Location<unknown>) => ReactNode redirect?: ReactNode
} }
export const defaultRedirect = (location?: Location<unknown>) => ( export const defaultRedirect = (
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location?.pathname } }} /> <Navigate to={getUserId() ? '/access_denied' : '/login'} />
) )
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => { export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
const available = isURLAvailable(join(root, path)) const available = isURLAvailable(join(root, path))
return ( return (
<Route <Route
{...other} {...other}
path={path} path={path}
component={available ? component : undefined} element={available ? children : redirect}
render={({ location }) => available ? children : redirect(location)}
/> />
) )
}) })

View File

@ -0,0 +1,67 @@
import { join } from 'path'
import { Navigate, Route, Routes, RoutesProps } from 'react-router-dom'
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useMemo } from 'react'
import { useRootPath } from '@asb/context'
import { getUserId, isURLAvailable } from '@utils'
export type PrivateRoutesProps = RoutesProps & {
root?: string
redirect?: ReactNode
elseRedirect?: string | string[]
}
export const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
export const defaultRedirect = (
<Navigate to={getDefaultRedirectPath()} />
)
export const PrivateRoutes = memo<PrivateRoutesProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
const rootContext = useRootPath()
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
const items = useMemo(() => Children.map(children, (child) => {
const element = child as ReactElement
let key = element.key?.toString()
if (!key) return <></>
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
// Ключ автоматический преобразуется в "(.+)\$ключ"
// Все ":" в ключе заменяются на "=2"
// TODO: улучшить метод нормализации ключа
const path = toAbsolute(key)
return (
<Route
key={key}
path={path}
element={isURLAvailable(path) ? cloneElement(element) : redirect}
/>
)
}) ?? [], [children, redirect, toAbsolute])
const defaultRoute = useMemo(() => {
const routes: string[] = []
if (Array.isArray(elseRedirect))
routes.push(...elseRedirect)
else if(elseRedirect)
routes.push(elseRedirect)
routes.push(...items.map((elm) => elm?.props?.path))
const firstAvailableRoute = routes.find((path) => path && isURLAvailable(path))
return firstAvailableRoute ? toAbsolute(firstAvailableRoute) : getDefaultRedirectPath()
}, [items, elseRedirect, toAbsolute])
return (
<Routes>
{items}
<Route path={'/'} element={(
<Navigate to={defaultRoute} />
)}/>
</Routes>
)
})
export default PrivateRoutes

View File

@ -1,75 +0,0 @@
import { join } from 'path'
import { Location } from 'history'
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useContext, useMemo } from 'react'
import { Redirect, Route, Switch, SwitchProps, useLocation } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { isURLAvailable } from '@utils/permissions'
import { getUserId } from '@utils/storage'
export type PrivateSwitchProps = SwitchProps & {
root?: string
redirect?: (location?: Location<unknown>) => ReactNode
elseRedirect?: string | string[]
}
const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
export const defaultRedirect = (location?: Location<unknown>) => (
<Redirect to={{ pathname: getDefaultRedirectPath(), state: { from: location?.pathname } }} />
)
export const PrivateSwitch = memo<PrivateSwitchProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
const rootContext = useContext(RootPathContext)
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const location = useLocation()
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
const items = useMemo(() => Children.toArray(children).map((child) => {
const element = child as ReactElement
let key = element.key?.toString()
if (!key) return null
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
// Ключ автоматический преобразуется в "(.+)\$ключ"
// Все ":" в ключе заменяются на "=2"
// TODO: улучшить метод нормализации ключа
const path = toAbsolute(key)
return (
<Route
key={key}
path={path}
render={({ location }) => isURLAvailable(path) ? cloneElement(element) : redirect(location)}
/>
)
}), [children, redirect, toAbsolute])
const defaultRoute = useMemo(() => {
if (!elseRedirect) {
const path = items.map((elm) => elm?.props.path).find((path) => path && isURLAvailable(path))
if (path) return path
} else if (Array.isArray(elseRedirect)) {
const path = elseRedirect.find((path) => {
if (!path) return false
return isURLAvailable(toAbsolute(path))
})
if (path) return toAbsolute(path)
} else if(elseRedirect && isURLAvailable(toAbsolute(elseRedirect))) {
return toAbsolute(elseRedirect)
}
return getDefaultRedirectPath()
}, [items, elseRedirect, toAbsolute])
return (
<Switch>
{items}
<Route path={'/'}>
<Redirect to={{ pathname: defaultRoute, state: { from: location.pathname } }} />
</Route>
</Switch>
)
})
export default PrivateSwitch

View File

@ -3,11 +3,11 @@ export { PrivateContent } from './PrivateContent' // TODO: Remove
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
export { PrivateDefaultRoute } from './PrivateDefaultRoute' export { PrivateDefaultRoute } from './PrivateDefaultRoute'
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu' export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
export { PrivateSwitch } from './PrivateSwitch' export { PrivateRoutes } from './PrivateRoutes'
export type { PrivateRouteProps } from './PrivateRoute' export type { PrivateRouteProps } from './PrivateRoute'
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute' export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu' export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
export type { PrivateSwitchProps } from './PrivateSwitch' export type { PrivateRoutesProps } from './PrivateRoutes'

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react' import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps, Tag } from 'antd'
import { DefaultOptionType, SelectValue } from 'antd/lib/select' import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { Select, SelectProps, Tag } from 'antd'
import { OmitExtends } from '@utils' import type { OmitExtends } from '@utils/types'
import { columnPropsOther, DataType, makeColumn } from '.' import { columnPropsOther, DataType, makeColumn } from '.'

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { formatTime } from '@utils/datetime' import { formatTime } from '@utils'
import { makeColumn, columnPropsOther } from '.' import { makeColumn, columnPropsOther } from '.'
import { makeTimeSorter, TimePickerWrapper, TimePickerWrapperProps } from '..' import { makeTimeSorter, TimePickerWrapper, TimePickerWrapperProps } from '..'

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react' import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps } from 'antd' import { Select, SelectProps } from 'antd'
import { OmitExtends } from '@utils' import { findTimezoneId, rawTimezones, TimezoneId } from '@utils'
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils/datetime' import type { OmitExtends } from '@utils/types'
import { SimpleTimezoneDto } from '@api' import { SimpleTimezoneDto } from '@api'
import { columnPropsOther, makeColumn } from '.' import { columnPropsOther, makeColumn } from '.'

View File

@ -1,16 +1,16 @@
import { memo } from 'react' import { memo, ReactNode } from 'react'
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import { NamePath, Rule } from 'rc-field-form/lib/interface' import { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & { type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
editing?: boolean editing?: boolean
dataIndex?: NamePath dataIndex?: NamePath
input?: React.Component input?: ReactNode
isRequired?: boolean isRequired?: boolean
title: string title: string
formItemClass?: string formItemClass?: string
formItemRules?: Rule[] formItemRules?: Rule[]
children: React.ReactNode children: ReactNode
initialValue: any initialValue: any
} }

View File

@ -2,9 +2,8 @@ import { 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 } from 'antd'
import { OmitExtends } from '@utils' import type { OmitExtends } from '@utils/types'
import { getTableSettings, setTableSettings } from '@utils/storage' import { applyTableSettings, getTableSettings, setTableSettings, TableColumnSettings, TableSettings } from '@utils'
import { applySettings, ColumnSettings, TableSettings } from '@utils/table_settings'
import TableSettingsChanger from './TableSettingsChanger' import TableSettingsChanger from './TableSettingsChanger'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -12,7 +11,7 @@ import { tryAddKeys } from './EditableTable'
import '@styles/index.css' import '@styles/index.css'
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T> export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, ColumnSettings>[] export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
export type TableContainer = TableProps<any> & { export type TableContainer = TableProps<any> & {
columns: TableColumns columns: TableColumns
@ -33,7 +32,7 @@ export const Table = memo<TableContainer>(({ columns, dataSource, tableName, sho
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
useEffect(() => setNewColumns(() => { useEffect(() => setNewColumns(() => {
const newColumns = applySettings(columns, settings) const newColumns = applyTableSettings(columns, settings)
if (tableName && showSettingsChanger) { if (tableName && showSettingsChanger) {
const oldTitle = newColumns[0].title const oldTitle = newColumns[0].title
newColumns[0].title = (props) => ( newColumns[0].title = (props) => (

View File

@ -3,16 +3,16 @@ import { ColumnsType } from 'antd/lib/table'
import { Button, Modal, Switch, Table } from 'antd' import { Button, Modal, Switch, Table } from 'antd'
import { SettingOutlined } from '@ant-design/icons' import { SettingOutlined } from '@ant-design/icons'
import { ColumnSettings, makeSettings, mergeSettings, TableSettings } from '@utils/table_settings' import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettings } from '@utils'
import { TableColumns } from './Table' import { TableColumns } from './Table'
import { makeColumn } from '.' import { makeColumn } from '.'
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): ColumnSettings[] => { const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): TableColumnSettings[] => {
const newSettings = mergeSettings(makeSettings(columns ?? []), settings ?? {}) const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
return Object.values(newSettings).map((set, i) => ({ ...set, key: i })) return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
} }
const unparseSettings = (columns: ColumnSettings[]): TableSettings => const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
Object.fromEntries(columns.map((column) => [column.columnName, column])) Object.fromEntries(columns.map((column) => [column.columnName, column]))
export type TableSettingsChangerProps = { export type TableSettingsChangerProps = {
@ -24,8 +24,8 @@ export type TableSettingsChangerProps = {
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => { export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
const [visible, setVisible] = useState<boolean>(false) const [visible, setVisible] = useState<boolean>(false)
const [newSettings, setNewSettings] = useState<ColumnSettings[]>(parseSettings(columns, settings)) const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
const [tableColumns, setTableColumns] = useState<ColumnsType<ColumnSettings>>([]) const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
const onVisibilityChange = useCallback((index: number, visible: boolean) => { const onVisibilityChange = useCallback((index: number, visible: boolean) => {
setNewSettings((oldSettings) => { setNewSettings((oldSettings) => {
@ -52,7 +52,7 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button> <Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
</> </>
), ),
render: (visible: boolean, _?: ColumnSettings, index: number = NaN) => ( render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
<Switch <Switch
checked={visible} checked={visible}
checkedChildren={'Отображён'} checkedChildren={'Отображён'}

View File

@ -2,7 +2,7 @@ import { Moment } from 'moment'
import { TimePicker, TimePickerProps } from 'antd' import { TimePicker, TimePickerProps } from 'antd'
import { memo, useCallback, useMemo } from 'react' import { memo, useCallback, useMemo } from 'react'
import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils/datetime' 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'> & {

View File

@ -1,4 +1,4 @@
import { timeToMoment } from '@utils/datetime' import { timeToMoment } from '@utils'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import { TimeDto } from '@api' import { TimeDto } from '@api'

View File

@ -1,30 +1,31 @@
import { memo, MouseEventHandler, useCallback, useState } from 'react' import { memo, MouseEventHandler, useCallback, useState } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { Button, Dropdown, DropDownProps, Menu } from 'antd' import { Button, Dropdown, DropDownProps } from 'antd'
import { UserOutlined } from '@ant-design/icons' import { UserOutlined } from '@ant-design/icons'
import { getUserLogin, removeUser } from '@utils/storage' import { getUserLogin, removeUser } from '@utils'
import { ChangePassword } from './ChangePassword' import { ChangePassword } from './ChangePassword'
import { PrivateMenuItemLink } from './Private/PrivateMenuItem' import { PrivateMenu } from './Private'
import AdminPanel from '@pages/AdminPanel'
type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean } type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean }
export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => { export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false) const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const onChangePasswordClick: MouseEventHandler = useCallback((e) => { const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
setIsModalVisible(true) setIsModalVisible(true)
e.preventDefault() e.preventDefault()
}, []) }, [])
const onChangePasswordOk = useCallback(() => { const onChangePasswordOk = useCallback(() => {
setIsModalVisible(false) setIsModalVisible(false)
history.push({ pathname: '/login', state: { from: location.pathname }}) navigate('/login', { state: { from: location.pathname }})
}, [history, location]) }, [navigate, location])
return ( return (
<> <>
@ -32,19 +33,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
{...other} {...other}
placement={'bottomRight'} placement={'bottomRight'}
overlay={( overlay={(
<Menu style={{ textAlign: 'right' }}> <PrivateMenu style={{ textAlign: 'right' }}>
{isAdmin ? ( {isAdmin ? (
<PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/> <PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'}/>
) : ( ) : (
<PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/> <PrivateMenu.Link key={'admin'} path={'/admin'} title={'Панель администратора'} content={AdminPanel}/>
)} )}
<Menu.Item> <PrivateMenu.Link visible onClick={onChangePasswordClick} title={'Сменить пароль'} />
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link> <PrivateMenu.Link visible path={'/login'} onClick={removeUser} title={'Выход'} />
</Menu.Item> </PrivateMenu>
<Menu.Item>
<Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
</Menu.Item>
</Menu>
)} )}
> >
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button> <Button icon={<UserOutlined/>}>{getUserLogin()}</Button>

View File

@ -2,7 +2,7 @@ import { notification } from 'antd'
import { Dispatch, ReactNode, SetStateAction } from 'react' import { Dispatch, ReactNode, SetStateAction } from 'react'
import { isDev } from '@utils' import { isDev } from '@utils'
import { getUserToken } from '@utils/storage' import { getUserToken } from '@utils'
import { ApiError, FileInfoDto } from '@api' import { ApiError, FileInfoDto } from '@api'
const notificationTypeDictionary = new Map([ const notificationTypeDictionary = new Map([

View File

@ -2,7 +2,7 @@ import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { DepositService } from '@api' import { DepositService } from '@api'
export const getTreeData = async () => { export const getTreeData = async () => {
@ -40,17 +40,19 @@ export const WellSelector = memo(({ idWell, value, onChange, treeData, treeLabel
const [wellsTree, setWellsTree] = useState([]) const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([]) const [wellLabels, setWellLabels] = useState([])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const wellsTree = treeData ?? await getTreeData() async () => {
const labels = treeLabels ?? getTreeLabels(wellsTree) const wellsTree = treeData ?? await getTreeData()
setWellsTree(wellsTree) const labels = treeLabels ?? getTreeLabels(wellsTree)
setWellLabels(labels) setWellsTree(wellsTree)
}, setWellLabels(labels)
null, },
'Не удалось загрузить список скважин', null,
'Получение списка скважин' 'Не удалось загрузить список скважин',
), [idWell, treeData, treeLabels]) 'Получение списка скважин'
)
}, [idWell, treeData, treeLabels])
return ( return (
<TreeSelect <TreeSelect

View File

@ -3,7 +3,7 @@ import { LabelInValueType } from 'rc-select/lib/Select'
import { RawValueType } from 'rc-tree-select/lib/TreeSelect' import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
import { DefaultValueType } from 'rc-tree-select/lib/interface' import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode, useCallback, memo } from 'react' import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
@ -29,40 +29,47 @@ export type TreeNodeData = {
} }
const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => { const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => {
if (!value) return value const result = value?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
const type = value.replaceAll('/', ' ').trim().split(' ')[0] if (wellsTree.length <= 0 || !result) return
const [url, type] = result
let deposit: TreeNodeData | undefined let deposit: TreeNodeData | undefined
let cluster: TreeNodeData | undefined let cluster: TreeNodeData | undefined
let well: TreeNodeData | undefined let well: TreeNodeData | undefined
switch (type) { switch (type) {
case 'deposit':
deposit = wellsTree.find((deposit) => deposit.key === url)
if (deposit)
return `${deposit.title}`
return 'Ошибка! Месторождение не найдено!'
case 'cluster': case 'cluster':
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === value) cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === url)
)) ))
if (deposit && cluster) if (deposit && cluster)
return `${deposit.title} / ${cluster.title}` return `${deposit.title} / ${cluster.title}`
break return 'Ошибка! Куст не найден!'
case 'well': case 'well':
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => ( cluster = deposit.children?.find((cluster: TreeNodeData) => (
well = cluster.children?.find((well: TreeNodeData) => well.key === value) well = cluster.children?.find((well: TreeNodeData) => well.key === url)
)) ))
)) ))
if (deposit && cluster && well) if (deposit && cluster && well)
return `${deposit.title} / ${cluster.title} / ${well.title}` return `${deposit.title} / ${cluster.title} / ${well.title}`
break return 'Ошибка! Скважина не найдена!'
default: break default: break
} }
return 'Ошибка! Скважина не найдена!'
} }
export const WellTreeSelector = memo(({ ...other }) => { export const WellTreeSelector = memo(({ ...other }) => {
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([]) const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false) const [showLoader, setShowLoader] = useState<boolean>(false)
const [value, setValue] = useState<string>() const [value, setValue] = useState<string>()
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const routeMatch = useRouteMatch('/:route/:id')
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -99,14 +106,14 @@ export const WellTreeSelector = memo(({ ...other }) => {
) )
}, []) }, [])
useEffect(() => setValue(getLabel(wellsTree, routeMatch?.url)), [wellsTree, routeMatch]) useEffect(() => setValue(getLabel(wellsTree, location.pathname)), [wellsTree, location])
const onChange = useCallback((value?: string): void => setValue(getLabel(wellsTree, value)), [wellsTree]) const onChange = useCallback((value?: string): void => setValue(getLabel(wellsTree, value)), [wellsTree])
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => { const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
if (['number', 'string'].includes(typeof value)) if (['number', 'string'].includes(typeof value))
history.push({ pathname: String(value), state: { from: location.pathname }}) navigate(String(value), { state: { from: location.pathname }})
}, [history, location]) }, [navigate, location])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

View File

@ -1,7 +1,9 @@
import { memo } from 'react' import { memo } from 'react'
import logo from '@images/logo_32.png'
export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => ( export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => (
<img src={'/images/logo_32.png'} alt={'АСБ'} className={'logo'} {...props} /> <img src={logo} alt={'АСБ'} className={'logo'} {...props} />
)) ))
export default Logo export default Logo

0
public/images/logo_32.png → src/images/logo_32.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,15 +1,19 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import { createRoot } from 'react-dom/client'
import App from './App'
import reportWebVitals from './reportWebVitals' import reportWebVitals from './reportWebVitals'
import App from './App'
import '@styles/index.css' import '@styles/index.css'
ReactDOM.render(( const container = document.getElementById('root') ?? document.body
<React.StrictMode> const root = createRoot(container)
<App />
</React.StrictMode> root.render(
), document.getElementById('root')) <React.StrictMode>
<App />
</React.StrictMode>
)
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))

View File

@ -1,12 +1,12 @@
import { Result, Tooltip, Typography } from 'antd' import { Result, Typography } from 'antd'
import { memo } from 'react' import { memo } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { CloseCircleOutlined } from '@ant-design/icons' import { CloseCircleOutlined } from '@ant-design/icons'
const { Paragraph, Text } = Typography const { Paragraph, Text } = Typography
export const AccessDenied = memo(() => { export const AccessDenied = memo(() => {
const history = useHistory() const navigate = useNavigate()
return ( return (
<Result <Result
@ -28,7 +28,7 @@ export const AccessDenied = memo(() => {
<Paragraph> <Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} /> <CloseCircleOutlined style={{ color: 'red' }} />
&nbsp;Страницы не существует.&nbsp; &nbsp;Страницы не существует.&nbsp;
<Link to={'#'} onClick={history.goBack}>Вернуться назад &gt;</Link> <Link to={'#'} onClick={navigate(-1)}>Вернуться назад &gt;</Link>
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} /> <CloseCircleOutlined style={{ color: 'red' }} />

View File

@ -12,13 +12,12 @@ import {
} 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'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
import { coordsFixed } from './DepositController' import { coordsFixed } from './DepositController'
export const ClusterController = memo(() => { const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([]) const [clusters, setClusters] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -58,18 +57,22 @@ export const ClusterController = memo(() => {
'Получение списка кустов' 'Получение списка кустов'
), []) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
let deposits = arrayOrDefault(await AdminDepositService.getAll()) async () => {
deposits = deposits.map((deposit) => ({ value: deposit.id, label: deposit.caption })) let deposits = arrayOrDefault(await AdminDepositService.getAll())
setDeposits(deposits) deposits = deposits.map((deposit) => ({ value: deposit.id, label: deposit.caption }))
}, setDeposits(deposits)
setShowLoader, },
`Не удалось загрузить список месторождений`, setShowLoader,
'Получение списка месторождений' `Не удалось загрузить список месторождений`,
), []) 'Получение списка месторождений'
)
}, [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminClusterService, service: AdminClusterService,
@ -103,4 +106,8 @@ export const ClusterController = memo(() => {
) )
}) })
export default ClusterController export default wrapPrivateComponent(ClusterController, {
requirements: ['AdminDeposit.get', 'AdminCluster.get'],
title: 'Кусты',
route: 'cluster',
})

View File

@ -11,12 +11,10 @@ import {
} 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'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
const CompanyController = memo(() => {
export const CompanyController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [companies, setCompanies] = useState([]) const [companies, setCompanies] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -31,32 +29,34 @@ export const CompanyController = memo(() => {
setCompanies(arrayOrDefault(companies)) setCompanies(arrayOrDefault(companies))
}, []) }, [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async() => { invokeWebApiWrapperAsync(
const companyTypes = arrayOrDefault(await AdminCompanyTypeService.getAll()).map((companyType) => ({ async() => {
value: companyType.id, const companyTypes = arrayOrDefault(await AdminCompanyTypeService.getAll()).map((companyType) => ({
label: companyType.caption, value: companyType.id,
})) label: companyType.caption,
}))
setColumns([ setColumns([
makeColumn('Название', 'caption', { makeColumn('Название', 'caption', {
width: 200, width: 200,
editable: true, editable: true,
sorter: makeStringSorter('caption'), sorter: makeStringSorter('caption'),
formItemRules: min1, formItemRules: min1,
}), }),
makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, {
width: 200, width: 200,
editable: true editable: true
}), }),
]) ])
await updateTable() await updateTable()
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить список типов компаний`, `Не удалось загрузить список типов компаний`,
'Получение списка типов команд' 'Получение списка типов команд'
), [updateTable]) )
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminCompanyService, service: AdminCompanyService,
@ -95,4 +95,8 @@ export const CompanyController = memo(() => {
) )
}) })
export default CompanyController export default wrapPrivateComponent(CompanyController, {
requirements: ['AdminCompany.get', 'AdminCompanyType.get'],
title: 'Компании',
route: 'company',
})

View File

@ -9,10 +9,9 @@ import {
defaultPagination defaultPagination
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { AdminCompanyTypeService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'caption', { makeColumn('Название', 'caption', {
@ -23,7 +22,7 @@ const columns = [
}), }),
] ]
export const CompanyTypeController = memo(() => { const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([]) const [companyTypes, setCompanyTypes] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -42,7 +41,9 @@ export const CompanyTypeController = memo(() => {
'Получение списка типов компаний' 'Получение списка типов компаний'
), []) ), [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminCompanyTypeService, service: AdminCompanyTypeService,
@ -76,4 +77,8 @@ export const CompanyTypeController = memo(() => {
) )
}) })
export default CompanyTypeController export default wrapPrivateComponent(CompanyTypeController, {
requirements: ['AdminCompanyType.get'],
title: 'Типы компаний',
route: 'company_type',
})

View File

@ -3,9 +3,8 @@ import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table' import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { arrayOrDefault } from '@utils'
import { AdminDepositService } from '@api' import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-' export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
@ -17,7 +16,7 @@ const depositColumns = [
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]
export const DepositController = memo(() => { const DepositController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -39,7 +38,9 @@ export const DepositController = memo(() => {
'Получение списка месторождений' 'Получение списка месторождений'
), []) ), [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminDepositService, service: AdminDepositService,
@ -73,4 +74,8 @@ export const DepositController = memo(() => {
) )
}) })
export default DepositController export default wrapPrivateComponent(DepositController, {
requirements: ['AdminDeposit.get'],
title: 'Месторождения',
route: 'deposit',
})

View File

@ -8,10 +8,9 @@ import {
makeStringSorter makeStringSorter
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminPermissionService } from '@api' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { AdminPermissionService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'name', { makeColumn('Название', 'name', {
@ -26,7 +25,7 @@ const columns = [
}), }),
] ]
export const PermissionController = memo(() => { const PermissionController = memo(() => {
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -47,7 +46,9 @@ export const PermissionController = memo(() => {
'Получение списка прав' 'Получение списка прав'
), []) ), [])
useEffect(() => updateTable(), [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminPermissionService, service: AdminPermissionService,
@ -81,4 +82,8 @@ export const PermissionController = memo(() => {
) )
}) })
export default PermissionController export default wrapPrivateComponent(PermissionController, {
requirements: ['AdminPermission.get'],
title: 'Разрешения',
route: 'permission',
})

View File

@ -5,11 +5,10 @@ import { PermissionView, RoleView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table' import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table'
import { AdminPermissionService, AdminUserRoleService } from '@api' import { AdminPermissionService, AdminUserRoleService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
export const RoleController = memo(() => { const RoleController = memo(() => {
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [roles, setRoles] = useState([]) const [roles, setRoles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -38,16 +37,18 @@ export const RoleController = memo(() => {
setRoles(arrayOrDefault(roles)) setRoles(arrayOrDefault(roles))
}, []) }, [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const permissions = await AdminPermissionService.getAll() async () => {
setPermissions(arrayOrDefault(permissions)) const permissions = await AdminPermissionService.getAll()
await loadRoles() setPermissions(arrayOrDefault(permissions))
}, await loadRoles()
setShowLoader, },
`Не удалось загрузить список ролей`, setShowLoader,
'Получение списка ролей' `Не удалось загрузить список ролей`,
), [loadRoles]) 'Получение списка ролей'
)
}, [loadRoles])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminUserRoleService, service: AdminUserRoleService,
@ -85,4 +86,8 @@ export const RoleController = memo(() => {
) )
}) })
export default RoleController export default wrapPrivateComponent(RoleController, {
requirements: ['AdminPermission.get', 'AdminUserRole.get'],
title: 'Роли',
route: 'role',
})

View File

@ -8,8 +8,8 @@ import LoaderPortal from '@components/LoaderPortal'
import { lables } from '@components/views/TelemetryView' import { lables } from '@components/views/TelemetryView'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import TelemetrySelect from '@components/selectors/TelemetrySelect' import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { AdminTelemetryService } from '@api' import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
const { Item } = Descriptions const { Item } = Descriptions
@ -32,7 +32,7 @@ export const TelemetryInfo = memo(({ info, danger, ...other }) => (
</Descriptions> </Descriptions>
)) ))
export const TelemetryMerger = memo(() => { const TelemetryMerger = memo(() => {
const [primary, setPrimary] = useState(null) const [primary, setPrimary] = useState(null)
const [secondary, setSecondary] = useState(null) const [secondary, setSecondary] = useState(null)
const [telemetry, setTelemetry] = useState(null) const [telemetry, setTelemetry] = useState(null)
@ -68,7 +68,9 @@ export const TelemetryMerger = memo(() => {
'Объединение телеметрий', 'Объединение телеметрий',
), [updateTelemetry, secondary, primary]) ), [updateTelemetry, secondary, primary])
useEffect(updateTelemetry, [updateTelemetry]) useEffect(() => {
updateTelemetry()
}, [updateTelemetry])
useEffect(() => { useEffect(() => {
const query = new URLSearchParams(location.search) const query = new URLSearchParams(location.search)
@ -132,4 +134,9 @@ export const TelemetryMerger = memo(() => {
) )
}) })
export default TelemetryMerger export default wrapPrivateComponent(TelemetryMerger, {
requirements: [],
title: 'Объединение',
route: 'merger',
key: 'merger',
})

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { PullRequestOutlined } from '@ant-design/icons' import { PullRequestOutlined } from '@ant-design/icons'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
@ -13,18 +14,17 @@ import {
} from '@components/Table' } from '@components/Table'
import Poprompt from '@components/selectors/Poprompt' import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { AdminTelemetryService } from '@api' import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
import { useHistory } from 'react-router-dom'
export const TelemetryController = memo(() => { const TelemetryController = memo(() => {
const [telemetryData, setTelemetryData] = useState([]) const [telemetryData, setTelemetryData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const history = useHistory() const navigate = useNavigate()
const toMerger = useCallback((type, id) => () => history.push(`/admin/telemetry/merger/?${type}=${id}`), [history]) const toMerger = useCallback((type, id) => () => navigate(`/admin/telemetry/merger/?${type}=${id}`), [navigate])
const mergeRender = useCallback((value, record) => ( const mergeRender = useCallback((value, record) => (
<Poprompt <Poprompt
@ -81,20 +81,22 @@ export const TelemetryController = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase())) ].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [telemetryData, searchValue]) ), [telemetryData, searchValue])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll()) async () => {
setTelemetryData(telemetryData.map((telemetry) => ({ const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll())
...(telemetry?.info ?? []), setTelemetryData(telemetryData.map((telemetry) => ({
id: telemetry?.id, ...(telemetry?.info ?? []),
remoteUid: telemetry?.remoteUid, id: telemetry?.id,
realWell: telemetry?.well?.caption, remoteUid: telemetry?.remoteUid,
}))) realWell: telemetry?.well?.caption,
}, })))
setShowLoader, },
`Не удалось загрузить список телеметрии скважин`, setShowLoader,
'Полученик списка телеметрии скважин' `Не удалось загрузить список телеметрии скважин`,
), []) 'Полученик списка телеметрии скважин'
)
}, [])
return ( return (
<> <>
@ -118,4 +120,9 @@ export const TelemetryController = memo(() => {
) )
}) })
export default TelemetryController export default wrapPrivateComponent(TelemetryController, {
requirements: [],
title: 'Просмотр',
route: 'viewer',
key: 'viewer',
})

View File

@ -1,37 +1,34 @@
import { Layout } from 'antd' import { Layout } from 'antd'
import { lazy, memo, Suspense, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import { SuspenseFallback } from '@pages/SuspenseFallback' import TelemetryViewer from './TelemetryViewer'
import TelemetryMerger from './TelemetryMerger'
const TelemetryViewer = lazy(() => import('./TelemetryViewer')) const Telemetry = memo(() => {
const TelemetryMerger = lazy(() => import('./TelemetryMerger')) const root = useRootPath()
export const Telemetry = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/telemetry`, [root]) const rootPath = useMemo(() => `${root}/telemetry`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}> <PrivateMenu>
<PrivateMenu.Link key={'viewer'} title={'Просмотр'} /> <PrivateMenu.Link content={TelemetryViewer} />
<PrivateMenu.Link key={'merger'} title={'Объединение'} /> <PrivateMenu.Link content={TelemetryMerger} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content className={'site-layout-background'}> <Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}> <Routes>
<PrivateSwitch elseRedirect={['viewer', 'merger']}> <Route index element={<Navigate to={TelemetryViewer.route} replace />} />
<TelemetryViewer key={'viewer'} /> <Route path={'*'} element={<NoAccessComponent />} />
<TelemetryMerger key={'merger'} /> <Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
</PrivateSwitch> <Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
</Suspense> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </Layout>
@ -39,4 +36,9 @@ export const Telemetry = memo(() => {
) )
}) })
export default Telemetry export default wrapPrivateComponent(Telemetry, {
requirements: ['AdminTelemetry.get'],
title: 'Телеметрия',
key: 'telemetry',
route: 'telemetry/*',
})

View File

@ -17,15 +17,14 @@ import { ChangePassword } from '@components/ChangePassword'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api' import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules' import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/table' import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/filters'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import RoleTag from './RoleTag' import RoleTag from './RoleTag'
const SEARCH_TIMEOUT = 400 const SEARCH_TIMEOUT = 400
export const UserController = memo(() => { const UserController = memo(() => {
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [filteredUsers, setFilteredUsers] = useState([]) const [filteredUsers, setFilteredUsers] = useState([])
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -35,23 +34,25 @@ export const UserController = memo(() => {
const [selectedUser, setSelectedUser] = useState(null) const [selectedUser, setSelectedUser] = useState(null)
const [subject, setSubject] = useState(null) const [subject, setSubject] = useState(null)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const filteredUsers = users.filter((user) => user && (!searchValue || [ async () => {
user.login ?? '', const filteredUsers = users.filter((user) => user && (!searchValue || [
user.name ?? '', user.login ?? '',
user.surname ?? '', user.name ?? '',
user.partonymic ?? '', user.surname ?? '',
user.email ?? '', user.partonymic ?? '',
user.phone ?? '', user.email ?? '',
user.position ?? '', user.phone ?? '',
user.company?.caption ?? '', user.position ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))) user.company?.caption ?? '',
setFilteredUsers(filteredUsers) ].join(' ').toLowerCase().includes(searchValue.toLowerCase())))
}, setFilteredUsers(filteredUsers)
setIsSearching, },
`Не удалось произвести поиск пользователей` setIsSearching,
), [users, searchValue]) `Не удалось произвести поиск пользователей`
)
}, [users, searchValue])
useEffect(() => { useEffect(() => {
if (!subject) { if (!subject) {
@ -91,86 +92,88 @@ export const UserController = memo(() => {
'Получение списка пользователей' 'Получение списка пользователей'
), []) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const roles = arrayOrDefault(await AdminUserRoleService.getAll()) async () => {
const companies = arrayOrDefault(await AdminCompanyService.getAll()).map((company) => ({ const roles = arrayOrDefault(await AdminUserRoleService.getAll())
value: company.id, const companies = arrayOrDefault(await AdminCompanyService.getAll()).map((company) => ({
label: company.caption value: company.id,
})) label: company.caption
}))
const users = arrayOrDefault(await AdminUserService.getAll()) const users = arrayOrDefault(await AdminUserService.getAll())
setUsers(users) setUsers(users)
const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email']) const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email'])
const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))] const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))]
const rolesRender = (item) => item?.map((elm) => ( const rolesRender = (item) => item?.map((elm) => (
<Tag key={elm} color={'blue'}> <Tag key={elm} color={'blue'}>
<RoleView role={roles.find((role) => role.caption === elm)} /> <RoleView role={roles.find((role) => role.caption === elm)} />
</Tag> </Tag>
)) ?? '-' )) ?? '-'
setColumns([ setColumns([
makeTextColumn('Логин', 'login', null, null, null, { makeTextColumn('Логин', 'login', null, null, null, {
editable: true, editable: true,
formItemRules: [ formItemRules: [
{ required: true }, { required: true },
...createLoginRules, ...createLoginRules,
// () => ({ // () => ({
// validator(_, value) { // validator(_, value) {
// if (!value || users.findIndex((user) => user.login === value) < 0) // if (!value || users.findIndex((user) => user.login === value) < 0)
// return Promise.resolve() // return Promise.resolve()
// return Promise.reject(new Error('Логин уже занят!')) // return Promise.reject(new Error('Логин уже занят!'))
// } // }
// }) // })
// TODO: Для проверки уникальности логина необходимо исключить из выборки логин выбранного пользователя // TODO: Для проверки уникальности логина необходимо исключить из выборки логин выбранного пользователя
], ],
}), }),
makeTextColumn('Фамилия', 'surname', filters.surname, null, null, { makeTextColumn('Фамилия', 'surname', filters.surname, null, null, {
editable: true, 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, 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, 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, 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, editable: true,
formItemRules: phoneRules, formItemRules: phoneRules,
}), }),
makeTextColumn('Должность', 'position', null, null, null, { editable: true }), makeTextColumn('Должность', 'position', null, null, null, { editable: true }),
makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
editable: true, editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
onFilter: makeArrayOnFilter('roleNames'), onFilter: makeArrayOnFilter('roleNames'),
}), }),
makeSelectColumn('Компания', 'idCompany', companies, '--', { makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true, editable: true,
sorter: makeNumericSorter('idCompany'), sorter: makeNumericSorter('idCompany'),
}) })
]) ])
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить список компаний`, `Не удалось загрузить список компаний`,
'Получение списка компаний' 'Получение списка компаний'
), []) )
}, [])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminUserService, service: AdminUserService,
@ -215,4 +218,8 @@ export const UserController = memo(() => {
) )
}) })
export default UserController export default wrapPrivateComponent(UserController, {
requirements: ['AdminUser.get', 'AdminCompany.get', 'AdminUserRole.get'],
title: 'Пользователи',
route: 'user',
})

View File

@ -3,8 +3,8 @@ import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table' import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { RequestTrackerService } from '@api' import { RequestTrackerService } from '@api'
import { arrayOrDefault, formatDate } from '@utils'
const logRecordCount = 1000 const logRecordCount = 1000
@ -17,7 +17,7 @@ const columns = [
}), }),
] ]
export const VisitLog = memo(() => { const VisitLog = memo(() => {
const [logData, setLogData] = useState([]) const [logData, setLogData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -29,16 +29,18 @@ export const VisitLog = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase())) ].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [logData, searchValue]) ), [logData, searchValue])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const logData = arrayOrDefault(await RequestTrackerService.getUsersStat(logRecordCount)) async () => {
logData.forEach((log) => log.key = `${log.login}${log.ip}`) const logData = arrayOrDefault(await RequestTrackerService.getUsersStat(logRecordCount))
setLogData(logData) logData.forEach((log) => log.key = `${log.login}${log.ip}`)
}, setLogData(logData)
setShowLoader, },
`Не удалось загрузить список последних посещений пользователей`, setShowLoader,
'Получение списка последних посещений' `Не удалось загрузить список последних посещений пользователей`,
), []) 'Получение списка последних посещений'
)
}, [])
return ( return (
<> <>
@ -62,4 +64,8 @@ export const VisitLog = memo(() => {
) )
}) })
export default VisitLog export default wrapPrivateComponent(VisitLog, {
requirements: ['RequestTracker.get'],
title: 'Журнал посещений',
route: 'visit_log',
})

View File

@ -22,8 +22,7 @@ import {
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views' import { TelemetryView, CompanyView } from '@components/views'
import TelemetrySelect from '@components/selectors/TelemetrySelect' import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { coordsFixed } from '../DepositController' import { coordsFixed } from '../DepositController'
@ -37,7 +36,7 @@ const recordParser = (record) => ({
idTelemetry: record.telemetry?.id, idTelemetry: record.telemetry?.id,
}) })
export const WellController = memo(() => { const WellController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [wells, setWells] = useState([]) const [wells, setWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -74,51 +73,53 @@ export const WellController = memo(() => {
/> />
)) ))
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const companies = arrayOrDefault(await AdminCompanyService.getAll()) async () => {
const telemetry = arrayOrDefault(await AdminTelemetryService.getAll()) const companies = arrayOrDefault(await AdminCompanyService.getAll())
const clusters = arrayOrDefault(await AdminClusterService.getAll()).map((cluster) => ({ const telemetry = arrayOrDefault(await AdminTelemetryService.getAll())
value: cluster.id, const clusters = arrayOrDefault(await AdminClusterService.getAll()).map((cluster) => ({
label: cluster.caption value: cluster.id,
})) label: cluster.caption
}))
setColumns([ setColumns([
makeSelectColumn('Куст', 'idCluster', clusters, '--', { makeSelectColumn('Куст', 'idCluster', clusters, '--', {
width: '5rem', width: '5rem',
editable: true, editable: true,
sorter: makeNumericSorter('idCluster'), sorter: makeNumericSorter('idCluster'),
}), }),
makeColumn('Название', 'caption', { makeColumn('Название', 'caption', {
width: '5rem', width: '5rem',
editable: true, editable: true,
sorter: makeStringSorter('caption'), sorter: makeStringSorter('caption'),
}), }),
makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { makeSelectColumn('Тип', 'idWellType', wellTypes, '--', {
width: 150, width: 150,
editable: true, editable: true,
sorter: makeNumericSorter('idWellType'), sorter: makeNumericSorter('idWellType'),
}), }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Телеметрия', 'telemetry', { makeColumn('Телеметрия', 'telemetry', {
editable: true, editable: true,
render: (telemetry) => <TelemetryView telemetry={telemetry} />, render: (telemetry) => <TelemetryView telemetry={telemetry} />,
input: <TelemetrySelect telemetry={telemetry}/>, input: <TelemetrySelect telemetry={telemetry}/>,
}, ), }, ),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
editable: true, editable: true,
render: (company) => <CompanyView company={company} />, render: (company) => <CompanyView company={company} />,
}), }),
]) ])
await updateTable() await updateTable()
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить список кустов`, `Не удалось загрузить список кустов`,
'Получение списка кустов' 'Получение списка кустов'
), [updateTable]) )
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminWellService, service: AdminWellService,
@ -154,4 +155,8 @@ export const WellController = memo(() => {
) )
}) })
export default WellController export default wrapPrivateComponent(WellController, {
requirements: ['AdminCluster.get', 'AdminCompany.get', 'AdminTelemetry.get', 'AdminWell.get'],
title: 'Скважины',
route: 'well',
})

View File

@ -1,66 +1,69 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Layout } from 'antd' import { Layout } from 'antd'
import { lazy, memo, Suspense, useContext, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { AdminLayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import { SuspenseFallback } from '@pages/SuspenseFallback' import ClusterController from './ClusterController'
import CompanyController from './CompanyController'
import DepositController from './DepositController'
import UserController from './UserController'
import WellController from './WellController'
import RoleController from './RoleController'
import CompanyTypeController from './CompanyTypeController'
import PermissionController from './PermissionController'
import Telemetry from './Telemetry'
import VisitLog from './VisitLog'
const ClusterController = lazy(() => import( './ClusterController')) const AdminPanel = memo(() => {
const CompanyController = lazy(() => import( './CompanyController')) const root = useRootPath()
const DepositController = lazy(() => import( './DepositController'))
const UserController = lazy(() => import( './UserController'))
const WellController = lazy(() => import( './WellController'))
const RoleController = lazy(() => import( './RoleController'))
const CompanyTypeController = lazy(() => import('./CompanyTypeController'))
const PermissionController = lazy(() => import( './PermissionController'))
const TelemetrySection = lazy(() => import( './Telemetry'))
const VisitLog = lazy(() => import( './VisitLog'))
export const AdminPanel = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/admin`, [root]) const rootPath = useMemo(() => `${root}/admin`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <AdminLayoutPortal title={'Администраторская панель'}>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}> <PrivateMenu>
<PrivateMenu.Link key={'deposit' } title={'Месторождения' } /> <PrivateMenu.Link content={DepositController} />
<PrivateMenu.Link key={'cluster' } title={'Кусты' } /> <PrivateMenu.Link content={ClusterController} />
<PrivateMenu.Link key={'well' } title={'Скважины' } /> <PrivateMenu.Link content={WellController} />
<PrivateMenu.Link key={'user' } title={'Пользователи' } /> <PrivateMenu.Link content={UserController} />
<PrivateMenu.Link key={'company' } title={'Компании' } /> <PrivateMenu.Link content={CompanyController} />
<PrivateMenu.Link key={'company_type'} title={'Типы компаний' } /> <PrivateMenu.Link content={CompanyTypeController} />
<PrivateMenu.Link key={'role' } title={'Роли' } /> <PrivateMenu.Link content={RoleController} />
<PrivateMenu.Link key={'permission' } title={'Разрешения' } /> <PrivateMenu.Link content={PermissionController} />
<PrivateMenu.Link key={'telemetry' } title={'Телеметрия' } /> <PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link key={'visit_log' } title={'Журнал посещений'} /> <PrivateMenu.Link content={VisitLog} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content className={'site-layout-background'}> <Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}> <Routes>
<PrivateSwitch elseRedirect={['deposit', 'cluster', 'well', 'user', 'company', 'company_type', 'role', 'permission', 'telemetry', 'visit_log']}> <Route index element={<Navigate to={VisitLog.route} replace />} />
<DepositController key={'deposit'} /> <Route path={'*'} element={<NoAccessComponent />} />
<ClusterController key={'cluster'} /> <Route path={DepositController.route} element={<DepositController />} />
<WellController key={'well'} /> <Route path={ClusterController.route} element={<ClusterController />} />
<UserController key={'user'} /> <Route path={WellController.route} element={<WellController />} />
<CompanyController key={'company'} /> <Route path={UserController.route} element={<UserController />} />
<CompanyTypeController key={'company_type'} /> <Route path={CompanyController.route} element={<CompanyController />} />
<RoleController key={'role'} /> <Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
<PermissionController key={'permission'} /> <Route path={RoleController.route} element={<RoleController />} />
<TelemetrySection key={'telemetry/:tab?'} /> <Route path={PermissionController.route} element={<PermissionController />} />
<VisitLog key={'visit_log'} /> <Route path={Telemetry.route} element={<Telemetry />} />
</PrivateSwitch> <Route path={VisitLog.route} element={<VisitLog />} />
</Suspense> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </AdminLayoutPortal>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })
export default AdminPanel export default wrapPrivateComponent(AdminPanel, {
requirements: ['RequestTracker.get'],
title: 'Панель администратора',
route: 'admin/*',
key: 'admin',
})

View File

@ -1,14 +1,13 @@
import { Table as RawTable, Typography } from 'antd' import { Table as RawTable, Typography } from 'antd'
import { Fragment, memo, useCallback, useContext, useEffect, useState } from 'react' import { Fragment, memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { WellSelector } from '@components/selectors/WellSelector' import { WellSelector } from '@components/selectors/WellSelector'
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { OperationStatService, WellOperationService } from '@api' import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import '@styles/index.css' import '@styles/index.css'
import '@styles/statistics.less' import '@styles/statistics.less'
@ -64,7 +63,7 @@ const getWellData = async (wellsList) => {
return wellData return wellData
} }
export const Statistics = memo(() => { const Statistics = memo(() => {
const [sectionTypes, setSectionTypes] = useState([]) const [sectionTypes, setSectionTypes] = useState([])
const [avgColumns, setAvgColumns] = useState(defaultColumns) const [avgColumns, setAvgColumns] = useState(defaultColumns)
const [cmpColumns, setCmpColumns] = useState(defaultColumns) const [cmpColumns, setCmpColumns] = useState(defaultColumns)
@ -77,7 +76,7 @@ export const Statistics = memo(() => {
const [cmpData, setCmpData] = useState([]) const [cmpData, setCmpData] = useState([])
const [avgRow, setAvgRow] = useState({}) const [avgRow, setAvgRow] = useState({})
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const cmpSpeedRender = useCallback((key) => (section) => { const cmpSpeedRender = useCallback((key) => (section) => {
let spanClass = '' let spanClass = ''
@ -97,67 +96,75 @@ export const Statistics = memo(() => {
) )
}, [avgRow]) }, [avgRow])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const types = await WellOperationService.getSectionTypes(idWell) async () => {
setSectionTypes(Object.entries(types)) const types = await WellOperationService.getSectionTypes(idWell)
}, setSectionTypes(Object.entries(types))
setIsPageLoading, },
`Не удалось получить типы секции`, setIsPageLoading,
`Получение списка возможных секций`, `Не удалось получить типы секции`,
), [idWell]) `Получение списка возможных секций`,
)
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const filteredSections = avgData?.length > 0 ? sectionTypes.filter(([id, _]) => avgData.some((row) => `section_${id}` in row)) : sectionTypes async () => {
const filteredSections = avgData?.length > 0 ? sectionTypes.filter(([id, _]) => avgData.some((row) => `section_${id}` in row)) : sectionTypes
setAvgColumns([ setAvgColumns([
...defaultColumns, ...defaultColumns,
...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`)), ...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`)),
]) ])
setCmpColumns([ setCmpColumns([
...defaultColumns, ...defaultColumns,
...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`, { ...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`, {
speedRender: cmpSpeedRender(`section_${id}`) speedRender: cmpSpeedRender(`section_${id}`)
}))
])
},
setIsPageLoading,
'Не удалось установить необходимые столбцы'
)
}, [sectionTypes, avgData, cmpSpeedRender])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const avgData = await getWellData(avgWells)
setAvgData(avgData)
const avgRow = {}
avgData.forEach((row) => row && Object.keys(row).forEach((key) => {
if (!key.startsWith('section_')) return
if (!avgRow[key]) avgRow[key] = { depth: 0, time: 0, speed: 0, count: 0 }
avgRow[key].depth += row[key].depth ?? 0
avgRow[key].time += row[key].time ?? 0
avgRow[key].speed += row[key].speed ?? 0
avgRow[key].count++
})) }))
])
},
setIsPageLoading,
'Не удалось установить необходимые столбцы'
), [sectionTypes, avgData, cmpSpeedRender])
useEffect(() => invokeWebApiWrapperAsync( Object.values(avgRow).forEach((section) => section.speed /= section.count)
async () => {
const avgData = await getWellData(avgWells)
setAvgData(avgData)
const avgRow = {} setAvgRow(avgRow)
},
setIsAvgTableLoading,
'Не удалось загрузить данные для расчёта средних значений',
)
}, [avgWells])
avgData.forEach((row) => row && Object.keys(row).forEach((key) => { useEffect(() => {
if (!key.startsWith('section_')) return invokeWebApiWrapperAsync(
if (!avgRow[key]) avgRow[key] = { depth: 0, time: 0, speed: 0, count: 0 } async () => {
avgRow[key].depth += row[key].depth ?? 0 const cmpData = await getWellData(cmpWells)
avgRow[key].time += row[key].time ?? 0 setCmpData(cmpData)
avgRow[key].speed += row[key].speed ?? 0 },
avgRow[key].count++ setIsCmpTableLoading,
})) 'Не удалось получить скважины для сравнения',
)
Object.values(avgRow).forEach((section) => section.speed /= section.count) }, [cmpWells])
setAvgRow(avgRow)
},
setIsAvgTableLoading,
'Не удалось загрузить данные для расчёта средних значений',
), [avgWells])
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const cmpData = await getWellData(cmpWells)
setCmpData(cmpData)
},
setIsCmpTableLoading,
'Не удалось получить скважины для сравнения',
), [cmpWells])
const getStatisticsAvgSummary = useCallback((data) => ( const getStatisticsAvgSummary = useCallback((data) => (
<Summary fixed={'bottom'}> <Summary fixed={'bottom'}>
@ -234,4 +241,8 @@ export const Statistics = memo(() => {
) )
}) })
export default Statistics export default wrapPrivateComponent(Statistics, {
requirements: [],
title: 'Оценка по ЦБ',
route: 'statistics',
})

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Modal, Popconfirm } from 'antd' import { Button, Modal, Popconfirm, Tooltip } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Table } from '@components/Table' import { Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -15,33 +15,31 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
const [showParamsLoader, setShowParamsLoader] = useState(false) const [showParamsLoader, setShowParamsLoader] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false) const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => setParamsColumns(await getColumns(idWell)) invokeWebApiWrapperAsync(async () => setParamsColumns(await getColumns(idWell)))
), [idWell]) }, [idWell])
useEffect(() => console.log(paramsColumns), [paramsColumns])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync( const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
setIsParamsModalVisible(true) setIsParamsModalVisible(true)
const params = await DrillParamsService.getCompositeAll(idWell) const params = await DrillParamsService.getCompositeAll(idWell)
setParams(params) setParams(params)
}, },
setShowParamsLoader, setShowParamsLoader,
`Не удалось загрузить список режимов для скважины "${idWell}"`, `Не удалось загрузить список режимов для скважины "${idWell}"`,
'Получение списка режимов скважины' 'Получение списка режимов скважины'
), [idWell]) ), [idWell])
const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync( const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
await DrillParamsService.save(idWell, params) await DrillParamsService.save(idWell, params)
setIsParamsModalVisible(false) setIsParamsModalVisible(false)
}, },
setShowParamsLoader, setShowParamsLoader,
`Не удалось добавить режимы в список скважины "${idWell}"`, `Не удалось добавить режимы в список скважины "${idWell}"`,
'Добавление режима скважины' 'Добавление режима скважины'
), [idWell, params]) ), [idWell, params])
return ( return (
@ -61,7 +59,10 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
width={1700} width={1700}
footer={( footer={(
<Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}> <Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}>
<Button size={'large'} disabled={params.length <= 0}>Сохранить</Button> <Button
size={'large'}
disabled={params.length <= 0}
>Сохранить</Button>
</Popconfirm> </Popconfirm>
)} )}
> >

View File

@ -1,22 +1,23 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, useContext } from 'react' import { useState, useEffect, memo, useMemo } from 'react'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd' import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { CompanyView } from '@components/views' import { CompanyView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table' import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api' import { WellCompositeService } from '@api'
import { hasPermission } from '@utils/permissions'
import { import {
hasPermission,
wrapPrivateComponent,
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction, makeFilterMinMaxFunction,
getOperations getOperations
} from '@utils/functions' } from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd' import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from '@pages/Cluster/WellOperationsTable' import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
import NewParamsTable from './NewParamsTable' import NewParamsTable from './NewParamsTable'
@ -30,7 +31,7 @@ const sortBySectionId = (a, b) => a?.sectionId - b?.sectionId
const filtersSectionsType = [] const filtersSectionsType = []
const DAY_IN_MS = 1000 * 60 * 60 * 24 const DAY_IN_MS = 1000 * 60 * 60 * 24
export const WellCompositeSections = memo(({ statsWells, selectedSections }) => { const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
const [selectedWells, setSelectedWells] = useState([]) const [selectedWells, setSelectedWells] = useState([])
const [wellOperations, setWellOperations] = useState([]) const [wellOperations, setWellOperations] = useState([])
const [selectedWellsKeys, setSelectedWellsKeys] = useState([]) const [selectedWellsKeys, setSelectedWellsKeys] = useState([])
@ -39,7 +40,7 @@ export const WellCompositeSections = memo(({ statsWells, selectedSections }) =>
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const location = useLocation() const location = useLocation()
@ -239,4 +240,8 @@ export const WellCompositeSections = memo(({ statsWells, selectedSections }) =>
) )
}) })
export default WellCompositeSections export default wrapPrivateComponent(WellCompositeSections, {
requirements: ['WellComposite.get'],
title: 'Статистика по секциям',
route: 'sections',
})

View File

@ -1,23 +1,31 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { Col, Layout, Row } from 'antd' import { Col, Layout, Row } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import WellSelector from '@components/selectors/WellSelector' import WellSelector from '@components/selectors/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { OperationStatService, WellCompositeService } from '@api' import { OperationStatService, WellCompositeService } from '@api'
import ClusterWells from '@pages/Cluster/ClusterWells' import ClusterWells from '@pages/Cluster/ClusterWells'
import { WellCompositeSections } from './WellCompositeSections' import WellCompositeSections from './WellCompositeSections'
const { Content } = Layout const { Content } = Layout
export const WellCompositeEditor = memo(({ rootPath }) => { const properties = {
const { tab } = useParams() requirements: ['OperationStat.get', 'WellComposite.get'],
const idWell = useContext(IdWellContext) title: 'Композитная скважина',
route: 'composite/*',
key: 'composite',
}
const WellCompositeEditor = memo(() => {
const idWell = useIdWell()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/${properties.key}`, [root])
const [statsWells, setStatsWells] = useState([]) const [statsWells, setStatsWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -25,19 +33,21 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
const [selectedIdWells, setSelectedIdWells] = useState([]) const [selectedIdWells, setSelectedIdWells] = useState([])
const [selectedSections, setSelectedSections] = useState([]) const [selectedSections, setSelectedSections] = useState([])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
try { async () => {
const selected = await WellCompositeService.get(idWell) try {
setSelectedSections(arrayOrDefault(selected)) setSelectedSections(arrayOrDefault(await WellCompositeService.get(idWell)))
} catch(e) { } catch(e) {
setSelectedSections([]) setSelectedSections([])
} throw e
}, }
setShowLoader, },
'Не удалось загрузить список скважин', setShowLoader,
'Получение списка скважин' 'Не удалось загрузить список скважин',
), [idWell]) 'Получение списка скважин'
)
}, [idWell])
useEffect(() => { useEffect(() => {
const wellIds = selectedSections.map((value) => value.idWellSrc) const wellIds = selectedSections.map((value) => value.idWellSrc)
@ -45,15 +55,17 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
setSelectedIdWells(wellIds) setSelectedIdWells(wellIds)
}, [selectedSections]) }, [selectedSections])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells)) async () => {
setStatsWells(stats) const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
}, setStatsWells(stats)
setShowTabLoader, },
'Не удалось загрузить статистику по скважинам/секциям', setShowTabLoader,
'Получение статистики по скважинам/секциям' 'Не удалось загрузить статистику по скважинам/секциям',
), [selectedIdWells]) 'Получение статистики по скважинам/секциям'
)
}, [selectedIdWells])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -66,19 +78,22 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>
<PrivateMenu root={rootPath} mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}> <PrivateMenu root={rootPath} className={'well_menu'}>
<PrivateMenu.Link key={'wells'} title={'Статистика по скважинам'} /> <PrivateMenu.Link content={ClusterWells} />
<PrivateMenu.Link key={'sections'} title={'Статистика по секциям'} /> <PrivateMenu.Link content={WellCompositeSections} />
</PrivateMenu> </PrivateMenu>
</Col> </Col>
</Row> </Row>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<LoaderPortal show={showTabLoader}> <LoaderPortal show={showTabLoader}>
<PrivateSwitch root={rootPath} elseRedirect={['wells', 'sections']}> <Routes>
<ClusterWells key={'wells'} statsWells={statsWells} /> <Route index element={<Navigate to={ClusterWells.route} replace/>} />
<WellCompositeSections key={'sections'} statsWells={statsWells} selectedSections={selectedSections} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={ClusterWells.route} element={<ClusterWells statsWells={statsWells} />} />
<Route path={WellCompositeSections.route} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
</Routes>
</LoaderPortal> </LoaderPortal>
</Content> </Content>
</Layout> </Layout>
@ -86,4 +101,4 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
) )
}) })
export default WellCompositeEditor export default wrapPrivateComponent(WellCompositeEditor, properties)

View File

@ -1,31 +1,34 @@
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Statistics from './Statistics' import Statistics from './Statistics'
import WellCompositeEditor from './WellCompositeEditor' import WellCompositeEditor from './WellCompositeEditor'
export const Analytics = memo(() => { const Analytics = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/analytics`, [root]) const rootPath = useMemo(() => `${root}/analytics`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'composite'} title={'Композитная скважина'} /> <PrivateMenu.Link content={WellCompositeEditor} />
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} /> <PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} content={Statistics} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content> <Layout.Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={'composite'}> <Routes>
<WellCompositeEditor key={'composite/:tab?'} rootPath={`${rootPath}/composite`} /> <Route index element={<Navigate to={WellCompositeEditor.getKey()} replace />} />
<Statistics key={'statistics'} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={WellCompositeEditor.route} element={<WellCompositeEditor />} />
<Route path={Statistics.route} element={<Statistics />} />
</Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </Layout>
@ -33,4 +36,9 @@ export const Analytics = memo(() => {
) )
}) })
export default Analytics export default wrapPrivateComponent(Analytics, {
requirements: [],
title: 'Аналитика',
route: 'analytics/*',
key: 'analytics',
})

View File

@ -20,11 +20,12 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { import {
getOperations, getOperations,
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction isRawDate,
} from '@utils/functions' makeFilterMinMaxFunction,
import { isRawDate } from '@utils' wrapPrivateComponent
} from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd' import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from './WellOperationsTable' import WellOperationsTable from './WellOperationsTable'
const filtersMinMax = [ const filtersMinMax = [
@ -39,7 +40,7 @@ const ONLINE_DEADTIME = 600_000
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-' const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
const numericRender = makeNumericRender(1) const numericRender = makeNumericRender(1)
export const ClusterWells = memo(({ statsWells }) => { const ClusterWells = memo(({ statsWells }) => {
const [selectedWellId, setSelectedWellId] = useState(0) const [selectedWellId, setSelectedWellId] = useState(0)
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
@ -187,4 +188,8 @@ export const ClusterWells = memo(({ statsWells }) => {
) )
}) })
export default ClusterWells export default wrapPrivateComponent(ClusterWells, {
requirements: [],
title: 'Статистика по скважинам',
route: 'wells',
})

View File

@ -1,33 +1,43 @@
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { useParams } from 'react-router-dom'
import { arrayOrDefault } from '@utils' import { LayoutPortal } from '@components/Layout'
import { OperationStatService } from '@api'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { OperationStatService } from '@api'
import ClusterWells from './ClusterWells' import ClusterWells from './ClusterWells'
import { useParams } from 'react-router-dom'
export const Cluster = memo(() => { const Cluster = memo(() => {
const { idCluster } = useParams() const { idCluster } = useParams()
const [data, setData] = useState([]) const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const clusterData = await OperationStatService.getStatCluster(idCluster) async () => {
setData(arrayOrDefault(clusterData?.statsWells)) const clusterData = await OperationStatService.getStatCluster(idCluster)
}, setData(arrayOrDefault(clusterData?.statsWells))
setShowLoader, },
`Не удалось загрузить данные по кусту "${idCluster}"`, setShowLoader,
'Получение данных по кусту' `Не удалось загрузить данные по кусту "${idCluster}"`,
), [idCluster]) 'Получение данных по кусту'
)
}, [idCluster])
return ( return (
<LoaderPortal show={showLoader}> <LayoutPortal title={'Анализ скважин куста'}>
<ClusterWells statsWells={data} /> <LoaderPortal show={showLoader}>
</LoaderPortal> <ClusterWells statsWells={data} />
) </LoaderPortal>
</LayoutPortal>
)
}) })
export default Cluster export default wrapPrivateComponent(Cluster, {
requirements: ['OperationStat.get'],
title: 'Анализ скважин куста',
route: 'cluster/:idCluster/*',
key: 'cluster',
})

View File

@ -1,17 +1,20 @@
import { Map, Overlay } from 'pigeon-maps' import { Map, Overlay } from 'pigeon-maps'
import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { ClusterService } from '@api'
import { arrayOrDefault } from '@utils'
import { PointerIcon } from '@components/icons' import { PointerIcon } from '@components/icons'
import { LayoutPortal } from '@components/Layout'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
import { ClusterService } from '@api'
import '@styles/index.css' import '@styles/index.css'
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 } const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => { const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0) if ((clusters?.length ?? 0) <= 0)
return defaultViewParams return defaultViewParams
@ -33,49 +36,58 @@ const calcViewParams = (clusters) => {
// zoom min = 1 (mega far) // zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg) // 4 - full Russia (161.6 deg)
// 13.5 - Khanty-Mansiysk // 13.5 - Khanty-Mansiysk
const zoom = Math.min(Math.max(5, 5 + 5 / (maxDeg + 0.5)), 15) const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5))
return { center, zoom } return { center, zoom }
} }
export const Deposit = memo(() => { const Deposit = memo(() => {
const [clustersData, setClustersData] = useState([]) const [clustersData, setClustersData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams) const [viewParams, setViewParams] = useState(defaultViewParams)
const location = useLocation() const location = useLocation()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const data = await ClusterService.getClusters() async () => {
setClustersData(arrayOrDefault(data)) const data = await ClusterService.getClusters()
setViewParams(calcViewParams(data)) setClustersData(arrayOrDefault(data))
}, setViewParams(calcViewParams(data))
setShowLoader, },
`Не удалось загрузить список кустов`, setShowLoader,
'Получить список кустов' `Не удалось загрузить список кустов`,
), []) 'Получить список кустов'
)
}, [])
return ( return (
<LoaderPortal show={showLoader}> <LayoutPortal noSheet title={'Месторождение'}>
<div className={'h-100vh'}> <LoaderPortal show={showLoader}>
<Map {...viewParams}> <div className={'h-100vh'}>
{clustersData.map(cluster => ( <Map {...viewParams}>
<Overlay {clustersData.map(cluster => (
width={32} <Overlay
anchor={[cluster.latitude, cluster.longitude]} width={32}
key={`${cluster.latitude} ${cluster.longitude}`} anchor={[cluster.latitude, cluster.longitude]}
> key={`${cluster.latitude} ${cluster.longitude}`}
<Link to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}> >
<PointerIcon state={'active'} width={48} height={59} /> <Link to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
<span>{cluster.caption}</span> <PointerIcon state={'active'} width={48} height={59} />
</Link> <span>{cluster.caption}</span>
</Overlay> </Link>
))} </Overlay>
</Map> ))}
</div> </Map>
</LoaderPortal> </div>
</LoaderPortal>
</LayoutPortal>
) )
}) })
export default Deposit export default wrapPrivateComponent(Deposit, {
requirements: ['Cluster.get'],
title: 'Месторождение',
route: 'deposit/*',
key: 'deposit',
})

View File

@ -1,13 +1,13 @@
import { useState, useEffect, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, useMemo, useCallback } from 'react'
import { DatePicker, Button, Input } from 'antd' import { DatePicker, Button, Input } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { UploadForm } from '@components/UploadForm' import { UploadForm } from '@components/UploadForm'
import { CompanyView, UserView } from '@components/views' import { CompanyView, UserView } from '@components/views'
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table' import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { FileService } from '@api' import { FileService } from '@api'
const pageSize = 12 const pageSize = 12
@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, idWell: wellId, mimeTypes, heade
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idwellContext = useContext(IdWellContext) const idwellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idwellContext, [wellId, idwellContext]) const idWell = useMemo(() => wellId ?? idwellContext, [wellId, idwellContext])
const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory]) const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory])
@ -83,8 +83,13 @@ export const DocumentsTemplate = ({ idCategory, idWell: wellId, mimeTypes, heade
) )
}, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page]) }, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page])
useEffect(update, [update]) useEffect(() => {
useEffect(() => onChange?.(files), [files, onChange]) update()
}, [update])
useEffect(() => {
onChange?.(files)
}, [files, onChange])
const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => { const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => {
await FileService.delete(idWell, file.id) await FileService.delete(idWell, file.id)

View File

@ -1,10 +1,11 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { FolderOutlined } from '@ant-design/icons' import { FolderOutlined } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { getTabname, wrapPrivateComponent, NoAccessComponent } from '@utils'
import DocumentsTemplate from './DocumentsTemplate' import DocumentsTemplate from './DocumentsTemplate'
@ -23,9 +24,9 @@ export const documentCategories = [
{ id: 9, key: 'closingService', title: 'Сервис по заканчиванию скважины' }, { id: 9, key: 'closingService', title: 'Сервис по заканчиванию скважины' },
] ]
export const MenuDocuments = memo(() => { const MenuDocuments = memo(() => {
const { category } = useParams() const category = getTabname()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/document`, [root]) const rootPath = useMemo(() => `${root}/document`, [root])
return ( return (
@ -42,19 +43,30 @@ export const MenuDocuments = memo(() => {
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={documentCategories.map((cat) => cat.key)}> <Routes>
{documentCategories.length > 0 && (
<Route index element={<Navigate to={documentCategories[0].key} replace />} />
)}
<Route path={'*'} element={<NoAccessComponent />} />
{documentCategories.map(category => ( {documentCategories.map(category => (
<DocumentsTemplate <Route key={category.key} path={category.key} element={(
key={category.key} <DocumentsTemplate
idCategory={category.id} idCategory={category.id}
tableName={`documents_${category.key}`} tableName={`documents_${category.key}`}
/> />
)} />
))} ))}
</PrivateSwitch> </Routes>
</Content> </Content>
</Layout> </Layout>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })
export default MenuDocuments export default wrapPrivateComponent(MenuDocuments, {
requirements: [ 'Deposit.get', 'File.get' ],
title: 'Документы',
route: 'document/*',
key: 'document',
})

View File

@ -1,8 +1,8 @@
import { Form, Select } from 'antd' import { Form, Select } from 'antd'
import { FileAddOutlined } from '@ant-design/icons' import { FileAddOutlined } from '@ant-design/icons'
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import Poprompt from '@components/selectors/Poprompt' import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
@ -21,18 +21,20 @@ export const CategoryAdder = memo(({ categories, onUpdate, className, ...other }
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [showCatLoader, setShowCatLoader] = useState(false) const [showCatLoader, setShowCatLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
setOptions(categories.map((category) => ({ async () => {
label: category.name ?? category.shortName, setOptions(categories.map((category) => ({
value: category.id label: category.name ?? category.shortName,
}))) value: category.id
}, })))
setShowCatLoader, },
`Не удалось установить список доступных категорий для добавления` setShowCatLoader,
), [categories]) `Не удалось установить список доступных категорий для добавления`
)
}, [categories])
const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync( const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,8 +1,8 @@
import { Input, Modal, Radio } from 'antd' import { Input, Modal, Radio } from 'antd'
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs' import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -30,27 +30,32 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
const [subject, setSubject] = useState(null) const [subject, setSubject] = useState(null)
const [needUpdate, setNeedUpdate] = useState(false) const [needUpdate, setNeedUpdate] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => visible && setNeedUpdate(false), [visible]) useEffect(() => {
if (visible)
setNeedUpdate(false)
}, [visible])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const filteredUsers = users.filter(({ user }) => user && [ async () => {
user.login ?? '', const filteredUsers = users.filter(({ user }) => user && [
user.name ?? '', user.login ?? '',
user.surname ?? '', user.name ?? '',
user.partonymic ?? '', user.surname ?? '',
user.email ?? '', user.partonymic ?? '',
user.phone ?? '', user.email ?? '',
user.position ?? '', user.phone ?? '',
user.company?.caption ?? '', user.position ?? '',
].join(' ').toLowerCase().includes(searchValue)) user.company?.caption ?? '',
setFilteredUsers(filteredUsers) ].join(' ').toLowerCase().includes(searchValue))
}, setFilteredUsers(filteredUsers)
setIsSearching, },
`Не удалось произвести поиск пользователей` setIsSearching,
), [users, searchValue]) `Не удалось произвести поиск пользователей`
)
}, [users, searchValue])
useEffect(() => { useEffect(() => {
if (!subject) { if (!subject) {
@ -71,14 +76,16 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
} }
}, [subject]) }, [subject])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell)) async () => {
setAllUsers(allUsers) const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell))
}, setAllUsers(allUsers)
setShowLoader, },
`Не удалось загрузить список доступных пользователей скважины "${idWell}"` setShowLoader,
), [idWell]) `Не удалось загрузить список доступных пользователей скважины "${idWell}"`
)
}, [idWell])
const calcUsers = useCallback(() => { const calcUsers = useCallback(() => {
if (!visible) return if (!visible) return

View File

@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Button, DatePicker, Input, Modal } from 'antd' import { Button, DatePicker, Input, Modal } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { CompanyView } from '@components/views' import { CompanyView } from '@components/views'
import DownloadLink from '@components/DownloadLink' import DownloadLink from '@components/DownloadLink'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
@ -65,21 +65,23 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [companyName, setCompanyName] = useState('') const [companyName, setCompanyName] = useState('')
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
if (!visible) return async () => {
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] if (!visible) return
const skip = (page - 1) * pageSize const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
const skip = (page - 1) * pageSize
const paginatedHistory = await FileService.getFilesInfo(idWell, idCategory, companyName, fileName, begin, end, skip, pageSize) const paginatedHistory = await FileService.getFilesInfo(idWell, idCategory, companyName, fileName, begin, end, skip, pageSize)
setTotal(paginatedHistory?.count ?? 0) setTotal(paginatedHistory?.count ?? 0)
setData(arrayOrDefault(paginatedHistory?.items)) setData(arrayOrDefault(paginatedHistory?.items))
}, },
setIsLoading, setIsLoading,
`Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"` `Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"`
), [idWell, idCategory, visible, range, companyName, fileName, page, pageSize]) )
}, [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
const onPaginationChange = useCallback((page, pageSize) => { const onPaginationChange = useCallback((page, pageSize) => {
setPage(page) setPage(page)

View File

@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Button, Input, Popconfirm, Form } from 'antd' import { Button, Input, Popconfirm, Form } from 'antd'
import { import {
DeleteOutlined, DeleteOutlined,
@ -6,7 +6,7 @@ import {
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import UploadForm from '@components/UploadForm' import UploadForm from '@components/UploadForm'
import DownloadLink from '@components/DownloadLink' import DownloadLink from '@components/DownloadLink'
@ -45,7 +45,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
file // Информация о файле file // Информация о файле
} = partData ?? {} } = partData ?? {}
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory]) const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory])
const approvedMarks = useMemo(() => file?.fileMarks?.filter((mark) => mark.idMarkType === 1), [file]) const approvedMarks = useMemo(() => file?.fileMarks?.filter((mark) => mark.idMarkType === 1), [file])

View File

@ -13,7 +13,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useIdWell } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory' import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, formatDate } from '@utils' import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
import CategoryAdder from './CategoryAdder' import CategoryAdder from './CategoryAdder'
@ -41,7 +41,7 @@ const STATE_STRING = {
[ID_STATE.Unknown]: { icon: WarningOutlined, text: 'Неизвестно' }, [ID_STATE.Unknown]: { icon: WarningOutlined, text: 'Неизвестно' },
} }
export const DrillingProgram = memo(() => { const DrillingProgram = memo(() => {
const [selectedCategory, setSelectedCategory] = useState() const [selectedCategory, setSelectedCategory] = useState()
const [historyVisible, setHistoryVisible] = useState(false) const [historyVisible, setHistoryVisible] = useState(false)
const [editorVisible, setEditorVisible] = useState(false) const [editorVisible, setEditorVisible] = useState(false)
@ -79,7 +79,9 @@ export const DrillingProgram = memo(() => {
`Не удалось загрузить название скважины "${idWell}"` `Не удалось загрузить название скважины "${idWell}"`
), [idWell]) ), [idWell])
useEffect(() => updateData(), [updateData]) useEffect(() => {
updateData()
}, [updateData])
const onCategoryEdit = useCallback((catId) => { const onCategoryEdit = useCallback((catId) => {
setSelectedCategory(catId) setSelectedCategory(catId)
@ -172,4 +174,8 @@ export const DrillingProgram = memo(() => {
) )
}) })
export default DrillingProgram export default wrapPrivateComponent(DrillingProgram, {
requirements: [ 'DrillingProgram.get' ],
title: 'Программа бурения',
route: 'drillingProgram',
})

View File

@ -1,21 +1,21 @@
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom' import { Link, useNavigate, useLocation } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons' import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { Card, Form, Input, Button } from 'antd'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { loginRules, passwordRules } from '@utils/validationRules' import { loginRules, passwordRules } from '@utils/validationRules'
import { setUser } from '@utils/storage' import { setUser, wrapPrivateComponent } from '@utils'
import { AuthService } from '@api' import { AuthService } from '@api'
import '@styles/index.css' import '@styles/index.css'
import Logo from '@images/Logo' import Logo from '@images/Logo'
export const Login = memo(() => { const Login = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync( const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
@ -23,20 +23,19 @@ export const Login = memo(() => {
const user = await AuthService.login(formData) const user = await AuthService.login(formData)
if (!user) throw Error('Неправильный логин или пароль') if (!user) throw Error('Неправильный логин или пароль')
setUser(user) setUser(user)
console.log(location.state?.from) navigate(location.state?.from ?? '/deposit')
history.push(location.state?.from ?? 'well')
}, },
setShowLoader, setShowLoader,
(ex) => ex?.message ?? 'Ошибка входа', (ex) => ex?.message ?? 'Ошибка входа',
'Вход в систему' 'Вход в систему'
), [history, location]) ), [navigate, location])
return ( return (
<div className={'login_page shadow'}> <div className={'login_page shadow'}>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Logo style={{ marginBottom: '10px' }}/> <Logo style={{ marginBottom: '10px' }} />
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}> <Card bordered title={'Система мониторинга'} className={'shadow'} style={{ width: 350 }}>
<Form onFinish={handleLogin}> <Form onFinish={handleLogin}>
<Form.Item name={'login'} rules={loginRules}> <Form.Item name={'login'} rules={loginRules}>
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} /> <Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
@ -60,4 +59,8 @@ export const Login = memo(() => {
) )
}) })
export default Login export default wrapPrivateComponent(Login, {
requirements: [],
title: 'Вход в систему',
route: 'login',
})

View File

@ -1,45 +0,0 @@
import { memo } from 'react'
import { Route, Switch } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { AdminLayoutPortal, LayoutPortal } from '@components/Layout'
import { PrivateDefaultRoute, PrivateRoute } from '@components/Private'
import Well from './Well'
import Cluster from './Cluster'
import Deposit from './Deposit'
import AdminPanel from './AdminPanel'
import AccessDenied from './AccessDenied'
export const Main = memo(() => (
<RootPathContext.Provider value={''}>
<Switch>
<PrivateRoute path={'/admin/:tab?'}>
<AdminLayoutPortal title={'Администраторская панель'}>
<AdminPanel />
</AdminLayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/deposit'}>
<LayoutPortal noSheet title='Месторождение'>
<Deposit />
</LayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/cluster/:idCluster'}>
<LayoutPortal title={'Анализ скважин куста'}>
<Cluster />
</LayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/well/:idWell/:tab?'}>
<LayoutPortal>
<Well />
</LayoutPortal>
</PrivateRoute>
<Route path={'/access_denied'}>
<AccessDenied />
</Route>
<PrivateDefaultRoute urls={['/deposit']} />
</Switch>
</RootPathContext.Provider>
))
export default Main

View File

@ -1,4 +1,4 @@
import { useState, useEffect, memo, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { Button, Form, Input, Popconfirm, Timeline } from 'antd' import { Button, Form, Input, Popconfirm, Timeline } from 'antd'
import { import {
CheckSquareOutlined, CheckSquareOutlined,
@ -9,11 +9,10 @@ import {
DeleteOutlined DeleteOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, formatDate } from '@utils'
import { formatDate } from '@utils'
import { MeasureService } from '@api' import { MeasureService } from '@api'
import { View } from './View' import { View } from './View'
@ -33,7 +32,7 @@ export const MeasureTable = memo(({ group, updateMeasuresFunc, additionalButtons
const [isTableEditing, setIsTableEditing] = useState(false) const [isTableEditing, setIsTableEditing] = useState(false)
const [editingActionName, setEditingActionName] = useState('') const [editingActionName, setEditingActionName] = useState('')
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [measuresForm] = Form.useForm() const [measuresForm] = Form.useForm()

View File

@ -1,10 +1,11 @@
import { Button } from 'antd' import { useState, useEffect, memo } from 'react'
import { useState, useEffect, memo, useContext } from 'react'
import { TableOutlined } from '@ant-design/icons' import { TableOutlined } from '@ant-design/icons'
import { Button } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { MeasureService } from '@api' import { MeasureService } from '@api'
import { MeasureTable } from './MeasureTable' import { MeasureTable } from './MeasureTable'
@ -42,34 +43,36 @@ const defaultData = [
} }
] ]
export const Measure = memo(() => { const Measure = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true) const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true)
const [data, setData] = useState(defaultData) const [data, setData] = useState(defaultData)
const [tableIdx, setTableIdx] = useState(-1) const [tableIdx, setTableIdx] = useState(-1)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
if (!isMeasuresUpdating) return async () => {
const measures = await MeasureService.getHisory(idWell) if (!isMeasuresUpdating) return
setIsMeasuresUpdating(false) const measures = await MeasureService.getHisory(idWell)
setIsMeasuresUpdating(false)
setData(prevData => { setData(prevData => {
prevData.forEach(el => el.values = []) prevData.forEach(el => el.values = [])
measures.forEach(el => { measures.forEach(el => {
const idx = prevData.findIndex(group => el.idCategory === group.idCategory) const idx = prevData.findIndex(group => el.idCategory === group.idCategory)
if (idx >= 0) if (idx >= 0)
prevData[idx].values.push(el) prevData[idx].values.push(el)
})
return prevData
}) })
return prevData },
}) setShowLoader,
}, `Не удалось загрузить последние данные по скважине ${idWell}`,
setShowLoader, 'Получение последних данных телеметрий'
`Не удалось загрузить последние данные по скважине ${idWell}`, )
'Получение последних данных телеметрий' }, [idWell, isMeasuresUpdating])
), [idWell, isMeasuresUpdating])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -90,4 +93,8 @@ export const Measure = memo(() => {
) )
}) })
export default Measure export default wrapPrivateComponent(Measure, {
requirements: [ 'Measure.get' ],
title: 'Измерения',
route: 'measure',
})

View File

@ -1,5 +1,5 @@
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { import {
UserOutlined, UserOutlined,
@ -20,6 +20,7 @@ import {
passwordRules, passwordRules,
phoneRules phoneRules
} from '@utils/validationRules' } from '@utils/validationRules'
import { wrapPrivateComponent } from '@utils'
import Logo from '@images/Logo' import Logo from '@images/Logo'
@ -52,17 +53,17 @@ const createInput = (name, placeholder, rules, isPassword, dependencies) => (
export const Register = memo(() => { export const Register = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const navigate = useNavigate()
const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync( const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
await AuthService.register(formData) await AuthService.register(formData)
history.push('/login') navigate('/login')
}, },
setShowLoader, setShowLoader,
`Ошибка отправки заявки на регистрацию`, `Ошибка отправки заявки на регистрацию`,
'Отправка заявки на регистрацию' 'Отправка заявки на регистрацию'
), [history]) ), [navigate])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
@ -91,4 +92,8 @@ export const Register = memo(() => {
) )
}) })
export default Register export default wrapPrivateComponent(Register, {
requirements: [],
title: 'Регистрация',
route: 'register',
})

View File

@ -1,8 +1,8 @@
import moment from 'moment'
import { DatePicker, Descriptions, Divider, Form, Input, InputNumber, Modal, Select, Space, Table } from 'antd' import { DatePicker, Descriptions, Divider, Form, Input, InputNumber, Modal, Select, Space, Table } from 'antd'
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeColumn, makeGroupColumn } from '@components/Table' import { makeColumn, makeGroupColumn } from '@components/Table'
@ -133,7 +133,7 @@ export const ReportEditor = memo(({ visible, data, onDone, onCancel, checkIsDate
const [isInvalid, setIsInvalid] = useState(false) const [isInvalid, setIsInvalid] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const setFields = useCallback((data) => form.setFieldsValue(data ? { const setFields = useCallback((data) => form.setFieldsValue(data ? {
...data, ...data,

View File

@ -1,25 +1,25 @@
import moment from 'moment' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from 'antd'
import { FileExcelOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons' import { FileExcelOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { Button } from 'antd'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table' import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
import { download, invokeWebApiWrapperAsync } from '@components/factory' import { download, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { DailyReportService } from '@api' import { DailyReportService } from '@api'
import { arrayOrDefault } from '@utils'
import ReportEditor from './ReportEditor' import ReportEditor from './ReportEditor'
export const DailyReport = memo(() => { const DailyReport = memo(() => {
const [data, setData] = useState([]) const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [searchDate, setSearchDate] = useState([moment().subtract(1, 'week'), moment()]) const [searchDate, setSearchDate] = useState([moment().subtract(1, 'week'), moment()])
const [selectedReport, setSelectedReport] = useState(null) const [selectedReport, setSelectedReport] = useState(null)
const [isEditorVisible, setIsEditorVisible] = useState(false) const [isEditorVisible, setIsEditorVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateTable = useCallback(() => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -31,7 +31,9 @@ export const DailyReport = memo(() => {
'Получение списка суточных рапортов', 'Получение списка суточных рапортов',
), [idWell]) ), [idWell])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data]) const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data])
@ -111,4 +113,10 @@ export const DailyReport = memo(() => {
) )
}) })
export default DailyReport export default wrapPrivateComponent(DailyReport, {
requirements: [
// 'DailyReport.get',
],
title: 'Суточный рапорт',
route: 'daily_report',
})

View File

@ -1,8 +1,8 @@
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons' import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { Table, makeDateSorter, makeNumericSorter } from '@components/Table' import { Table, makeDateSorter, makeNumericSorter } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory'
@ -60,18 +60,20 @@ export const Reports = memo(() => {
const [reports, setReports] = useState([]) const [reports, setReports] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell) async () => {
const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date })) const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell)
setReports(reports) const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date }))
}, setReports(reports)
setShowLoader, },
`Не удалось загрузить список рапортов по скважине "${idWell}"`, setShowLoader,
'Получение списка рапортов' `Не удалось загрузить список рапортов по скважине "${idWell}"`,
), [idWell]) 'Получение списка рапортов'
)
}, [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

View File

@ -1,12 +1,13 @@
import 'moment/locale/ru' import 'moment/locale/ru'
import moment from 'moment' import moment from 'moment'
import { useState, useEffect, memo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { Radio, Button, Select, notification } from 'antd' import { Radio, Button, Select, notification } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { DateRangeWrapper } from 'components/Table' import { DateRangeWrapper } from 'components/Table'
import { LoaderPortal } from '@components/LoaderPortal' import { LoaderPortal } from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
import { ReportService } from '@api' import { ReportService } from '@api'
@ -33,7 +34,7 @@ const reportFormats = [
{ value: 1, label: 'LAS' }, { value: 1, label: 'LAS' },
] ]
export const DiagramReport = memo(() => { const DiagramReport = memo(() => {
const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()]) const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()])
const [filterDateRange, setFilterDateRange] = useState([ const [filterDateRange, setFilterDateRange] = useState([
moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day'),
@ -44,7 +45,7 @@ export const DiagramReport = memo(() => {
const [pagesCount, setPagesCount] = useState(0) const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync( const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
@ -88,48 +89,52 @@ export const DiagramReport = memo(() => {
!current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]') !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
, [aviableDateRange]) , [aviableDateRange])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const datesRangeResponse = await ReportService.getReportsDateRange(idWell) async () => {
if (!datesRangeResponse?.from || !datesRangeResponse.to) const datesRangeResponse = await ReportService.getReportsDateRange(idWell)
throw new Error('Формат ответа неверный!') if (!datesRangeResponse?.from || !datesRangeResponse.to)
throw new Error('Формат ответа неверный!')
const datesRange = [ const datesRange = [
moment(datesRangeResponse.from), moment(datesRangeResponse.from),
moment(datesRangeResponse.to), moment(datesRangeResponse.to),
] ]
setAviableDateRange(datesRange) setAviableDateRange(datesRange)
const from = moment.max(moment(datesRange[1]).subtract(1, 'days'), datesRange[0]) const from = moment.max(moment(datesRange[1]).subtract(1, 'days'), datesRange[0])
setFilterDateRange([ setFilterDateRange([
from.startOf('day'), from.startOf('day'),
moment(datesRange[1]).startOf('day'), moment(datesRange[1]).startOf('day'),
]) ])
}, },
setShowLoader, setShowLoader,
`Не удалось получить диапозон дат рапортов для скважины "${idWell}"`, `Не удалось получить диапозон дат рапортов для скважины "${idWell}"`,
'Получение диапозона дат рапортов' 'Получение диапозона дат рапортов'
), [idWell]) )
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
if (filterDateRange?.length !== 2) return async () => {
const pagesCount = await ReportService.getReportSize( if (filterDateRange?.length !== 2) return
idWell, const pagesCount = await ReportService.getReportSize(
step, idWell,
format, step,
filterDateRange[0].toISOString(), format,
filterDateRange[1].toISOString() filterDateRange[0].toISOString(),
) filterDateRange[1].toISOString()
setPagesCount(pagesCount) )
}, setPagesCount(pagesCount)
setShowLoader, },
`Не удалось получить предварительные параметры отчета c setShowLoader,
${filterDateRange[0].format(dateTimeFormat)} по `Не удалось получить предварительные параметры отчета c
${filterDateRange[1].format(dateTimeFormat)}`, ${filterDateRange[0].format(dateTimeFormat)} по
'Получение размера рапортов' ${filterDateRange[1].format(dateTimeFormat)}`,
), [filterDateRange, step, format, idWell]) 'Получение размера рапортов'
)
}, [filterDateRange, step, format, idWell])
return ( return (
<div> <div>
@ -174,4 +179,8 @@ export const DiagramReport = memo(() => {
) )
}) })
export default DiagramReport export default wrapPrivateComponent(DiagramReport, {
requirements: [ 'Report.get' ],
title: 'Диаграмма',
route: 'diagram_report',
})

View File

@ -1,36 +1,38 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { FilePdfOutlined } from '@ant-design/icons' import { FilePdfOutlined } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import DailyReport from './DailyReport' import DailyReport from './DailyReport'
import DiagramReport from './DiagramReport' import DiagramReport from './DiagramReport'
const { Content } = Layout const { Content } = Layout
export const Reports = memo(() => { const Reports = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/reports`, [root]) const rootPath = useMemo(() => `${root}/reports`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'diagram_report'} icon={<FilePdfOutlined />} title={'Диаграмма'}/> <PrivateMenu.Link content={DiagramReport} icon={<FilePdfOutlined />} />
<PrivateMenu.Link key={'daily_report'} title={'Суточный рапорт'} /> <PrivateMenu.Link content={DailyReport} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['diagram_report', 'daily_report']}> <Routes>
<DiagramReport key={'diagram_report'} /> <Route index element={<Navigate to={'diagram_report'} replace />} />
<DailyReport key={'daily_report'} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={DiagramReport.route} element={<DiagramReport />} />
<Route path={DailyReport.route} element={<DailyReport />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>
@ -38,4 +40,9 @@ export const Reports = memo(() => {
) )
}) })
export default Reports export default wrapPrivateComponent(Reports, {
requirements: [],
title: 'Рапорта',
route: 'reports/*',
key: 'reports',
})

View File

@ -1,14 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, memo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Flex } from '@components/Grid' import { Flex } from '@components/Grid'
import { CopyUrlButton } from '@components/CopyUrl'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { range, wrapPrivateComponent } from '@utils'
import { TelemetryDataSaubService } from '@api' import { TelemetryDataSaubService } from '@api'
import { range } from '@utils'
import { normalizeData } from '../TelemetryView' import { normalizeData } from '../TelemetryView'
import { ArchiveDisplay, cutData } from './ArchiveDisplay' import { ArchiveDisplay, cutData } from './ArchiveDisplay'
@ -56,15 +58,20 @@ const getLoadingInterval = (loaded, startDate, interval) => {
} }
} }
export const Archive = memo(() => { const Archive = memo(() => {
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
const [chartInterval, setChartInterval] = useState(parseInt(defaultPeriod) * 1000)
const [startDate, setStartDate] = useState(new Date(Date.now() - chartInterval))
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [loaded, setLoaded] = useState(null) const [loaded, setLoaded] = useState(null)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [search, setSearchParams] = useSearchParams()
const getInitialRange = useCallback(() => parseInt(search.get('range') ?? defaultPeriod) * 1000, [search])
const getInitialDate = useCallback(() => new Date(search.get('start') ?? (Date.now() - chartInterval)), [search])
const [chartInterval, setChartInterval] = useState(getInitialRange)
const [startDate, setStartDate] = useState(getInitialDate)
const onGraphWheel = useCallback((e) => { const onGraphWheel = useCallback((e) => {
if (loaded && dateLimit.from && dateLimit.to) { if (loaded && dateLimit.from && dateLimit.to) {
@ -102,25 +109,34 @@ export const Archive = memo(() => {
}) })
}), [dateLimit]) }), [dateLimit])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { const params = {}
let dates = await TelemetryDataSaubService.getDataDatesRange(idWell) if (startDate)
dates = { params.start = startDate.toISOString()
from: new Date(dates?.from ?? 0), if (chartInterval)
to: new Date(dates?.to ?? 0) params.range = chartInterval / 1000
} setSearchParams(params)
setDateLimit(dates) }, [startDate, chartInterval])
setStartDate(new Date(Math.max(dates.from, +dates.to - chartInterval)))
},
setShowLoader,
`Не удалось загрузить диапозон телеметрии для скважины "${idWell}"`,
'Загрузка диапозона телеметрии'
), [])
useEffect(() => { useEffect(() => {
setStartDate((startDate) => new Date(Math.min(Date.now() - chartInterval, startDate))) invokeWebApiWrapperAsync(
setDataSaub([]) async () => {
}, [chartInterval]) let dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
dates = {
from: new Date(dates?.from ?? 0),
to: new Date(dates?.to ?? 0)
}
setDateLimit(dates)
},
setShowLoader,
`Не удалось загрузить диапозон телеметрии для скважины "${idWell}"`,
'Загрузка диапозона телеметрии'
)
}, [])
useEffect(() => {
setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval))))
}, [chartInterval, dateLimit])
useEffect(() => { useEffect(() => {
if (showLoader) return if (showLoader) return
@ -150,6 +166,11 @@ export const Archive = memo(() => {
) )
}, [idWell, chartInterval, loaded, startDate]) }, [idWell, chartInterval, loaded, startDate])
const onRangeChange = useCallback((value) => {
setChartInterval(value * 1000)
setDataSaub([])
}, [])
return ( return (
<> <>
<Flex style={{margin: '8px 8px 0'}}> <Flex style={{margin: '8px 8px 0'}}>
@ -164,8 +185,9 @@ export const Archive = memo(() => {
</div> </div>
<div style={{ marginLeft: '1rem' }}> <div style={{ marginLeft: '1rem' }}>
Период:&nbsp; Период:&nbsp;
<PeriodPicker onChange={(val) => setChartInterval(val * 1000)} /> <PeriodPicker value={chartInterval / 1000} onChange={onRangeChange} />
</div> </div>
<CopyUrlButton style={{ marginLeft: '1rem' }} />
</Flex> </Flex>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<ArchiveDisplay <ArchiveDisplay
@ -179,4 +201,8 @@ export const Archive = memo(() => {
) )
}) })
export default Archive export default wrapPrivateComponent(Archive, {
requirements: ['TelemetryDataSaub.get'],
title: 'Архив',
route: 'archive',
})

View File

@ -1,14 +1,13 @@
import { memo, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { CloseOutlined } from '@ant-design/icons' import { CloseOutlined } from '@ant-design/icons'
import { Button, Menu, Popconfirm } from 'antd' import { Button, Menu, Popconfirm } from 'antd'
import { IdWellContext, RootPathContext } from '@asb/context' import { useIdWell, useRootPath } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { BaseWidget, WidgetSettingsWindow } from '@components/widgets' import { BaseWidget, WidgetSettingsWindow } from '@components/widgets'
import { getJSON, setJSON } from '@utils/storage' import { arrayOrDefault, wrapPrivateComponent, getJSON, setJSON, getTabname } from '@utils'
import { arrayOrDefault } from '@utils'
import Subscribe from '@services/signalr' import Subscribe from '@services/signalr'
import { import {
WitsInfoService, WitsInfoService,
@ -88,7 +87,7 @@ const groupsReducer = (groups, action) => {
break break
case 'add_widget': case 'add_widget':
if (groupIdx >= 0) if (groupIdx >= 0 && widgetIdx < 0)
newGroups[groupIdx].widgets.push(value) newGroups[groupIdx].widgets.push(value)
break break
case 'edit_widget': case 'edit_widget':
@ -106,21 +105,21 @@ const groupsReducer = (groups, action) => {
return newGroups return newGroups
} }
export const DashboardNNB = memo(() => { const DashboardNNB = memo(({ enableEditing = false }) => {
const [groups, dispatchGroups] = useReducer(groupsReducer, []) const [groups, dispatchGroups] = useReducer(groupsReducer, [])
const [witsInfo, setWitsInfo] = useState([]) const [witsInfo, setWitsInfo] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [selectedSettings, setSelectedSettings] = useState(null) const [selectedSettings, setSelectedSettings] = useState(null)
const [values, setValues] = useState({}) const [values, setValues] = useState({})
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/dashboard_nnb`, [root]) const rootPath = useMemo(() => `${root}/dashboard_nnb`, [root])
const history = useHistory() const navigate = useNavigate()
const { tab: selectedGroup } = useParams() const selectedGroup = getTabname()
if (!selectedGroup && groups?.length > 0) if (!selectedGroup && groups?.length > 0)
history.push(`${rootPath}/${groups[0].id}`) navigate(`${rootPath}/${groups[0].id}`)
const group = useMemo(() => ({ const group = useMemo(() => ({
@ -128,16 +127,18 @@ export const DashboardNNB = memo(() => {
...groups.find(({ id }) => `${id}` === selectedGroup), ...groups.find(({ id }) => `${id}` === selectedGroup),
}), [groups, selectedGroup]) }), [groups, selectedGroup])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const info = await getWitsInfo() async () => {
setWitsInfo(info) const info = await getWitsInfo()
dispatchGroups({ type: 'init', witsInfo: info }) setWitsInfo(info)
}, dispatchGroups({ type: 'init', witsInfo: info })
setIsLoading, },
'Не удалось загрузить информацию о параметрах ННБ', setIsLoading,
'Получение информации о параметрах ННБ' 'Не удалось загрузить информацию о параметрах ННБ',
), []) 'Получение информации о параметрах ННБ'
)
}, [])
const handleData = useCallback((data, recordId) => { const handleData = useCallback((data, recordId) => {
const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {}) const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {})
@ -175,8 +176,8 @@ export const DashboardNNB = memo(() => {
const removeGroup = useCallback((id) => { const removeGroup = useCallback((id) => {
dispatchGroups({ type: 'remove_group', groupId: `${id}` }) dispatchGroups({ type: 'remove_group', groupId: `${id}` })
if (id === selectedGroup) history.push(`${rootPath}`) if (id === selectedGroup) navigate(`${rootPath}`)
}, [rootPath, history, selectedGroup]) }, [rootPath, navigate, selectedGroup])
const addWidget = useCallback((settings) => dispatchGroups({ const addWidget = useCallback((settings) => dispatchGroups({
type: 'add_widget', type: 'add_widget',
@ -209,17 +210,21 @@ export const DashboardNNB = memo(() => {
selectable={true} selectable={true}
selectedKeys={[selectedGroup]} selectedKeys={[selectedGroup]}
> >
<Menu.Item key={'add_group'}> {enableEditing && (
<AddGroupWindow addGroup={addGroup} /> <>
</Menu.Item> <Menu.Item key={'add_group'}>
{group?.editable && ( <AddGroupWindow addGroup={addGroup} />
<Menu.Item key={'add_widget'}> </Menu.Item>
<AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} /> {group?.editable && (
</Menu.Item> <Menu.Item key={'add_widget'}>
<AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} />
</Menu.Item>
)}
</>
)} )}
{groups.map(({ id, name, editable }) => ( {groups.map(({ id, name, editable }) => (
<Menu.Item key={id}> <Menu.Item key={id}>
{editable && ( {enableEditing && editable && (
<Popconfirm <Popconfirm
title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'} title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'}
onConfirm={() => removeGroup(id)} onConfirm={() => removeGroup(id)}
@ -229,7 +234,7 @@ export const DashboardNNB = memo(() => {
<Button type={'link'} icon={<CloseOutlined />} /> <Button type={'link'} icon={<CloseOutlined />} />
</Popconfirm> </Popconfirm>
)} )}
<Button type={'text'} style={{ paddingLeft: 0 }} onClick={() => history.push(`${rootPath}/${id}`)}>{name}</Button> <Button type={'text'} style={{ paddingLeft: 0 }} onClick={() => navigate(`${rootPath}/${id}`)}>{name}</Button>
</Menu.Item> </Menu.Item>
))} ))}
</Menu> </Menu>
@ -238,7 +243,7 @@ export const DashboardNNB = memo(() => {
<BaseWidget <BaseWidget
key={widget.id} key={widget.id}
// onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование // onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование
onRemove={group.editable && removeWidget} onRemove={ enableEditing && group.editable && removeWidget}
{...widget} {...widget}
value={values[widget.recordId]?.[widget.witsId]} value={values[widget.recordId]?.[widget.witsId]}
/> />
@ -255,4 +260,17 @@ export const DashboardNNB = memo(() => {
) )
}) })
export default DashboardNNB export default wrapPrivateComponent(DashboardNNB, {
requirements: [
// 'WitsInfo',
// 'WitsRecord1',
// 'WitsRecord7',
// 'WitsRecord8',
// 'WitsRecord50',
// 'WitsRecord60',
// 'WitsRecord61',
],
title: 'ННБ',
route: 'dashboard_nnb/*',
key: 'dashboard_nnb',
})

View File

@ -1,10 +1,13 @@
import { useState, useEffect, memo, useCallback, useContext } from 'react'
import { Table, Select, DatePicker, Input } from 'antd' import { Table, Select, DatePicker, Input } from 'antd'
import { useState, useEffect, memo, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeColumn, makeDateColumn, makeNumericSorter } from '@components/Table' import { makeColumn, makeDateColumn, makeNumericSorter } from '@components/Table'
import { wrapPrivateComponent } from '@utils'
import { MessageService } from '@api' import { MessageService } from '@api'
@ -47,7 +50,7 @@ const filterOptions = [
const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>) const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>)
// Данные для таблицы // Данные для таблицы
export const Messages = memo(() => { const Messages = memo(() => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
const [pagination, setPagination] = useState(null) const [pagination, setPagination] = useState(null)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
@ -56,32 +59,39 @@ export const Messages = memo(() => {
const [searchString, setSearchString] = useState('') const [searchString, setSearchString] = useState('')
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const navigate = useNavigate()
const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), []) const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] async () => {
const skip = (page - 1) * pageSize const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
const paginatedMessages = await MessageService.getMessages(idWell, skip, pageSize, categories, begin, end, searchString) const skip = (page - 1) * pageSize
if (!paginatedMessages) return const paginatedMessages = await MessageService.getMessages(idWell, skip, pageSize, categories, begin, end, searchString)
if (!paginatedMessages) return
setMessages(paginatedMessages.items.map(m => ({ setMessages(paginatedMessages.items.map(m => ({
key: m.id, key: m.id,
categoryids: categoryDictionary[m.categoryId], categoryids: categoryDictionary[m.categoryId],
begin: m.date, begin: m.date,
...m ...m
}))) })))
setPagination({ setPagination({
total: paginatedMessages.count, total: paginatedMessages.count,
current: Math.floor(paginatedMessages.skip / pageSize), current: Math.floor(paginatedMessages.skip / pageSize),
}) })
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить сообщения по скважине "${idWell}"`, `Не удалось загрузить сообщения по скважине "${idWell}"`,
'Полученик списка сообщений' 'Полученик списка сообщений'
), [idWell, page, categories, range, searchString]) )
}, [idWell, page, categories, range, searchString])
const onMessageRow = useCallback((record) => ({
onClick: () => navigate(`/well/${idWell}/telemetry/archive?range=1800&start=${moment(record?.date).subtract(3, 'minute').local().toISOString()}`)
}), [idWell, navigate])
return ( return (
<> <>
@ -118,10 +128,15 @@ export const Messages = memo(() => {
}} }}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={'messages'} tableName={'messages'}
onRow={onMessageRow}
/> />
</LoaderPortal> </LoaderPortal>
</> </>
) )
}) })
export default Messages export default wrapPrivateComponent(Messages, {
requirements: ['Message.get'],
title: 'Сообщения',
route: 'messages',
})

View File

@ -2,7 +2,7 @@ import { memo, useCallback, useMemo, useState } from 'react'
import { Button, Modal } from 'antd' import { Button, Modal } from 'antd'
import { EditableTable, makeActionHandler, makeTextColumn } from '@components/Table' import { EditableTable, makeActionHandler, makeTextColumn } from '@components/Table'
import { getPermissions } from '@utils/permissions' import { getPermissions } from '@utils'
import { DrillerService } from '@api' import { DrillerService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const reqRule = [{ message: 'Обязательное поле!', required: true }]

View File

@ -11,7 +11,7 @@ import {
makeSelectColumn, makeSelectColumn,
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { getPermissions } from '@utils/permissions' import { getPermissions } from '@utils'
import { ScheduleService } from '@api' import { ScheduleService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const reqRule = [{ message: 'Обязательное поле!', required: true }]

View File

@ -29,8 +29,13 @@ export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30
const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий
useEffect(() => d3.select(axisX.current).call(d3.axisBottom(x)), [axisX, x]) // Рисуем ось X useEffect(() => {
useEffect(() => d3.select(axisY.current).call(d3.axisLeft(y)), [axisY, y]) // Рисуем ось Y d3.select(axisX.current).call(d3.axisBottom(x))
}, [axisX, x]) // Рисуем ось X
useEffect(() => {
d3.select(axisY.current).call(d3.axisLeft(y))
}, [axisY, y]) // Рисуем ось Y
return ( return (
<div className={'page-left'} ref={setRef}> <div className={'page-left'} ref={setRef}>

View File

@ -7,8 +7,7 @@ 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 { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import { getPermissions } from '@utils/permissions' import { getPermissions, arrayOrDefault, range, wrapPrivateComponent } from '@utils'
import { arrayOrDefault, range } from '@utils'
import DrillerList from './DrillerList' import DrillerList from './DrillerList'
import DrillerSchedule from './DrillerSchedule' import DrillerSchedule from './DrillerSchedule'
@ -17,7 +16,7 @@ import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less' import '@styles/detected_operations.less'
export const Operations = memo(() => { const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [dateRange, setDateRange] = useState([]) const [dateRange, setDateRange] = useState([])
const [yDomain, setYDomain] = useState(20) const [yDomain, setYDomain] = useState(20)
@ -53,30 +52,34 @@ export const Operations = memo(() => {
updateDrillers() updateDrillers()
}, [updateDrillers, permissions]) }, [updateDrillers, permissions])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const dates = await TelemetryDataSaubService.getDataDatesRange(idWell) async () => {
if (dates) { const dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
const dt = [moment(dates.from), moment(dates.to)] if (dates) {
setDateRange(dt) const dt = [moment(dates.from), moment(dates.to)]
setDates(dt) setDateRange(dt)
} setDates(dt)
}, }
setIsLoading, },
'Не удалось загрузить диапазон доступных дат', setIsLoading,
'Получение дапазона доступних дат', 'Не удалось загрузить диапазон доступных дат',
), [idWell]) 'Получение дапазона доступних дат',
)
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
if (!dates) return async () => {
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString()) if (!dates) return
setData(data) const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
}, setData(data)
setIsLoading, },
'Не удалось загрузить список определённых операций', setIsLoading,
'Получение списка определённых операций', 'Не удалось загрузить список определённых операций',
), [idWell, dates]) 'Получение списка определённых операций',
)
}, [idWell, dates])
return ( return (
<div className={'container detected-operations-page'}> <div className={'container detected-operations-page'}>
@ -116,4 +119,11 @@ export const Operations = memo(() => {
) )
}) })
export default Operations export default wrapPrivateComponent(Operations, {
requirements: [
// 'DetectedOperation.get',
'TelemetryDataSaub.get',
],
title: 'Операции',
route: 'operations',
})

View File

@ -1,7 +1,7 @@
import { Table } from 'antd' import { Table } from 'antd'
import { useState, useEffect, useCallback, memo, useContext } from 'react' import { useState, useEffect, useCallback, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
@ -15,7 +15,7 @@ export const ActiveMessagesOnline = memo(() => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
const [loader, setLoader] = useState(false) const [loader, setLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleReceiveMessages = useCallback((messages) => { const handleReceiveMessages = useCallback((messages) => {
if (messages) if (messages)

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Select, Modal, Input, InputNumber } from 'antd' import { Select, Modal, Input, InputNumber } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } 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'
@ -16,7 +16,7 @@ export const SetpointSender = memo(({ onClose, visible, setpointNames }) => {
const [setpoints, setSetpoints] = useState([]) const [setpoints, setSetpoints] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const addingColumns = useMemo(() => [ const addingColumns = useMemo(() => [
{ {

View File

@ -1,14 +1,12 @@
import { Button, Modal } from 'antd' import { Button, Modal } from 'antd'
import { useState, useEffect, memo, useCallback, useMemo, useContext } from 'react' import { useState, useEffect, memo, useCallback, useMemo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Table } from '@components/Table' import { Table } from '@components/Table'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, makeStringCutter, formatDate } from '@utils'
import { makeStringCutter } from '@utils/string'
import { formatDate } from '@utils'
import { SetpointsService } from '@api' import { SetpointsService } from '@api'
import SetpointSender from './SetpointSender' import SetpointSender from './SetpointSender'
@ -23,22 +21,24 @@ export const Setpoints = memo(({ ...other }) => {
const [selected, setSelected] = useState(null) const [selected, setSelected] = useState(null)
const [setpointNames, setSetpointNames] = useState([]) const [setpointNames, setSetpointNames] = useState([])
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const names = await SetpointsService.getSetpointsNamesByIdWell(idWell) async () => {
if (!names) throw Error('Setpoints not found') const names = await SetpointsService.getSetpointsNamesByIdWell(idWell)
setSetpointNames(names.map(spn => ({ if (!names) throw Error('Setpoints not found')
label: spn.displayName, setSetpointNames(names.map(spn => ({
value: spn.name, label: spn.displayName,
tooltip: spn.comment value: spn.name,
}))) tooltip: spn.comment
}, })))
setIsLoading, },
`Не удалось загрузить список имён уставок по скважине "${idWell}"`, setIsLoading,
'Получение списка имён уставок' `Не удалось загрузить список имён уставок по скважине "${idWell}"`,
), [idWell]) 'Получение списка имён уставок'
)
}, [idWell])
const showMore = useCallback((id) => { const showMore = useCallback((id) => {
const selected = setpoints.find((sp) => sp.id === id) const selected = setpoints.find((sp) => sp.id === id)

View File

@ -1,6 +1,6 @@
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { WirelineView } from '@components/views' import { WirelineView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryWirelineRunOutService } from '@api' import { TelemetryWirelineRunOutService } from '@api'
@ -10,7 +10,7 @@ export const WirelineRunOut = memo(() => {
const [twro, setTwro] = useState({}) const [twro, setTwro] = useState({})
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const update = useCallback(() => invokeWebApiWrapperAsync( const update = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -25,7 +25,9 @@ export const WirelineRunOut = memo(() => {
if (visible) update() if (visible) update()
}, [update]) }, [update])
useEffect(update, [update]) useEffect(() => {
update()
}, [update])
return ( return (
<WirelineView <WirelineView

View File

@ -1,6 +1,14 @@
import { Select } from 'antd' import { Select } from 'antd'
import { useState, useEffect, useCallback, useContext } from 'react' import { useState, useEffect, useCallback, memo } from 'react'
import { useIdWell } from '@asb/context'
import { makeDateSorter } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { hasPermission, wrapPrivateComponent } from '@utils'
import { Subscribe } from '@services/signalr'
import { import {
DrillFlowChartService, DrillFlowChartService,
OperationStatService, OperationStatService,
@ -8,14 +16,6 @@ import {
TelemetryDataSpinService, TelemetryDataSpinService,
WellService WellService
} from '@api' } from '@api'
import { IdWellContext } from '@asb/context'
import { makeDateSorter } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { hasPermission } from '@utils/permissions'
import { Subscribe } from '@services/signalr'
import { MonitoringColumn } from './MonitoringColumn' import { MonitoringColumn } from './MonitoringColumn'
import { CustomColumn } from './CustomColumn' import { CustomColumn } from './CustomColumn'
@ -304,7 +304,7 @@ export const normalizeData = (data) => data?.map(item => ({
blockSpeed: Math.abs(item.blockSpeed) blockSpeed: Math.abs(item.blockSpeed)
})) ?? [] })) ?? []
export default function TelemetryView() { const TelemetryView = memo(() => {
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([]) const [dataSpin, setDataSpin] = useState([])
const [chartInterval, setChartInterval] = useState(defaultPeriod) const [chartInterval, setChartInterval] = useState(defaultPeriod)
@ -313,7 +313,7 @@ export default function TelemetryView() {
const [flowChartData, setFlowChartData] = useState([]) const [flowChartData, setFlowChartData] = useState([])
const [rop, setRop] = useState(null) const [rop, setRop] = useState(null)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleDataSaub = useCallback((data) => { const handleDataSaub = useCallback((data) => {
if (data) { if (data) {
@ -347,17 +347,19 @@ export default function TelemetryView() {
return unsubscribe return unsubscribe
}, [idWell, chartInterval, handleDataSpin, handleDataSaub]) }, [idWell, chartInterval, handleDataSpin, handleDataSaub])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const well = await WellService.get(idWell) async () => {
const rop = await OperationStatService.getClusterRopStatByIdWell(idWell) const well = await WellService.get(idWell)
setRop(rop) const rop = await OperationStatService.getClusterRopStatByIdWell(idWell)
setWellData(well ?? {}) setRop(rop)
}, setWellData(well ?? {})
setShowLoader, },
`Не удалось загрузить данные по скважине "${idWell}"`, setShowLoader,
'Получение данных по скважине' `Не удалось загрузить данные по скважине "${idWell}"`,
), [idWell]) 'Получение данных по скважине'
)
}, [idWell])
const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync( const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
async () => { async () => {
@ -428,4 +430,16 @@ export default function TelemetryView() {
</Grid> </Grid>
</LoaderPortal> </LoaderPortal>
) )
} })
export default wrapPrivateComponent(TelemetryView, {
requirements: [
'DrillFlowChart.get',
'OperationStat.get',
'TelemetryDataSaub.get',
'TelemetryDataSpin.get',
'Well.get',
],
title: 'Мониторинг',
route: 'telemetry',
})

View File

@ -1,46 +1,49 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { Layout } from 'antd' import { Layout } from 'antd'
import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons' import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateSwitch, PrivateMenu } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Archive from './Archive' import Archive from './Archive'
import Messages from './Messages' import Messages from './Messages'
import Operations from './Operations'
import DashboardNNB from './DashboardNNB' import DashboardNNB from './DashboardNNB'
import TelemetryView from './TelemetryView' import TelemetryView from './TelemetryView'
import '@styles/index.css' import '@styles/index.css'
import Operations from './Operations'
const { Content } = Layout const { Content } = Layout
export const Telemetry = memo(() => { const Telemetry = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/telemetry`, [root]) const rootPath = useMemo(() => `${root}/telemetry`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'monitoring'} icon={<FundViewOutlined />} title={'Мониторинг'}/> <PrivateMenu.Link content={TelemetryView} icon={<FundViewOutlined />} />
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} /> <PrivateMenu.Link content={Messages} icon={<AlertOutlined/>} />
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} /> <PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} /> <PrivateMenu.Link content={DashboardNNB} />
<PrivateMenu.Link key={'operations'} title={'Операции'} /> <PrivateMenu.Link content={Operations} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['monitoring', 'messages', 'archive', 'dashboard_nnb']}> <Routes>
<TelemetryView key={'monitoring'} /> <Route index element={<Navigate to={TelemetryView.route} replace />} />
<Messages key={'messages'} /> <Route path={'*'} element={<NoAccessComponent />} />
<Archive key={'archive'} />
<DashboardNNB key={'dashboard_nnb/:tab?'} /> <Route path={TelemetryView.route} element={<TelemetryView />} />
<Operations key={'operations'}/> <Route path={Messages.route} element={<Messages />} />
</PrivateSwitch> <Route path={Archive.route} element={<Archive />} />
<Route path={DashboardNNB.route} element={<DashboardNNB />} />
<Route path={Operations.route} element={<Operations />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>
@ -48,4 +51,10 @@ export const Telemetry = memo(() => {
) )
}) })
export default Telemetry export default wrapPrivateComponent(Telemetry, {
requirements: [],
icon: <FundViewOutlined />,
title: 'Телеметрия',
route: 'telemetry/*',
key: 'telemetry',
})

View File

@ -1,16 +1,17 @@
import { memo, useContext, useMemo } from 'react'
import { import {
FolderOutlined, FolderOutlined,
FundViewOutlined,
FilePdfOutlined, FilePdfOutlined,
ExperimentOutlined, ExperimentOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { useParams } from 'react-router-dom' import { memo, useMemo } from 'react'
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
import { IdWellContext, RootPathContext } from '@asb/context' import { IdWellContext, RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { LayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Measure from './Measure' import Measure from './Measure'
import Reports from './Reports' import Reports from './Reports'
@ -24,42 +25,50 @@ import '@styles/index.css'
const { Content } = Layout const { Content } = Layout
export const Well = memo(() => { const Well = memo(() => {
const { idWell, tab } = useParams() const { idWell } = useParams()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell]) const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell])
return ( return (
<RootPathContext.Provider value={rootPath}> <LayoutPortal>
<Layout> <RootPathContext.Provider value={rootPath}>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'telemetry'} icon={<FundViewOutlined />} title={'Телеметрия'}/> <PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link key={'reports'} icon={<FilePdfOutlined />} title={'Рапорта'} /> <PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
<PrivateMenu.Link key={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} /> <PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
<PrivateMenu.Link key={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} /> <PrivateMenu.Link content={WellOperations} icon={<FolderOutlined />} />
<PrivateMenu.Link key={'document'} icon={<FolderOutlined />} title={'Документы'} /> <PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
<PrivateMenu.Link key={'measure'} icon={<ExperimentOutlined />} title={'Измерения'} /> <PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
<PrivateMenu.Link key={'drillingProgram'} icon={<FolderOutlined />} title={'Программа бурения'} /> <PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
</PrivateMenu> </PrivateMenu>
<IdWellContext.Provider value={idWell}> <IdWellContext.Provider value={idWell}>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['telemetry', 'reports', 'analytics', 'operations', 'telemetryAnalysis', 'document', 'measure', 'drillingProgram']}> <Routes>
<Telemetry key={'telemetry/:tab?'} /> <Route index element={<Navigate to={Telemetry.getKey()} replace />} />
<Reports key={'reports/:tab?'} /> <Route path={'*'} element={<NoAccessComponent />} />
<Analytics key={'analytics/:tab?'} />
<WellOperations key={'operations/:tab?'} /> <Route path={Telemetry.route} element={<Telemetry />} />
<Documents key={'document/:category?'} /> <Route path={Reports.route} element={<Reports />} />
<Measure key={'measure'} /> <Route path={Analytics.route} element={<Analytics />} />
<DrillingProgram key={'drillingProgram'} /> <Route path={WellOperations.route} element={<WellOperations />} />
</PrivateSwitch> <Route path={Documents.route} element={<Documents />} />
<Route path={Measure.route} element={<Measure />} />
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</IdWellContext.Provider> </IdWellContext.Provider>
</Layout> </RootPathContext.Provider>
</RootPathContext.Provider> </LayoutPortal>
) )
}) })
export default Well export default wrapPrivateComponent(Well, {
requirements: [],
title: 'Скважина',
route: 'well/:idWell/*',
key: 'well',
})

View File

@ -1,6 +1,6 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeNumericMinMax, makeNumericMinMax,
@ -8,8 +8,7 @@ import {
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, arrayOrDefault } from '@utils'
import { arrayOrDefault } from '@utils'
import { DrillFlowChartService } from '@api' import { DrillFlowChartService } from '@api'
@ -26,7 +25,7 @@ export const DrillProcessFlow = memo(() => {
const [flows, setFlows] = useState([]) const [flows, setFlows] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateFlows = () => invokeWebApiWrapperAsync( const updateFlows = () => invokeWebApiWrapperAsync(
async () => { async () => {
@ -38,7 +37,9 @@ export const DrillProcessFlow = memo(() => {
'Получение режимно-технологической карты скважины' 'Получение режимно-технологической карты скважины'
) )
useEffect(updateFlows, [idWell]) useEffect(() => {
updateFlows()
}, [idWell])
const onAdd = async (flow) => { const onAdd = async (flow) => {
flow.idWell = idWell flow.idWell = idWell
@ -62,8 +63,8 @@ export const DrillProcessFlow = memo(() => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={flows} dataSource={flows}
tableName={'well_operations_flow'} tableName={'well_operations_flow'}

View File

@ -1,17 +1,21 @@
import { memo, useState } from 'react' import { memo, useMemo, useState } from 'react'
import { Button, Tooltip, Modal } from 'antd' import { Button, Tooltip, Modal } from 'antd'
import { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons' import { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons'
import { useIdWell } from '@asb/context'
import { download } from '@components/factory' import { download } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { ImportOperations } from './ImportOperations' import { ImportOperations } from './ImportOperations'
const style = { margin: 4 } const style = { margin: 4 }
export const ImportExportBar = memo(({ idWell, onImported, disabled }) => { export const ImportExportBar = memo(({ idWell: wellId, onImported, disabled }) => {
const [isImportModalVisible, setIsImportModalVisible] = useState(false) const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [idWellContext])
const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`) const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`)
const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`) const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`)

View File

@ -3,7 +3,7 @@ import { memo, useEffect, useMemo, useState } from 'react'
import { makeNumericRender } from '@components/Table' import { makeNumericRender } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime' import { formatDate, fractionalSum } from '@utils'
import '@styles/tvd.less' import '@styles/tvd.less'
@ -26,27 +26,29 @@ export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
.reduce((out, row) => out + (row?.durationHours ?? 0), 0) .reduce((out, row) => out + (row?.durationHours ?? 0), 0)
, [operations.fact]) , [operations.fact])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const [factStartDate, factEndDate] = calcEndDate(operations.fact) async () => {
const [planStartDate, planEndDate] = calcEndDate(operations.plan) const [factStartDate, factEndDate] = calcEndDate(operations.fact)
const [predictStartDate, predictEndDate] = calcEndDate(operations.predict) const [planStartDate, planEndDate] = calcEndDate(operations.plan)
const [predictStartDate, predictEndDate] = calcEndDate(operations.predict)
const last = predictEndDate ?? factEndDate const last = predictEndDate ?? factEndDate
setAdditionalData({ setAdditionalData({
lag: (+new Date(last) - +new Date(planEndDate)) / 86400_000, lag: (+new Date(last) - +new Date(planEndDate)) / 86400_000,
endDate: last, endDate: last,
factStartDate, factStartDate,
factEndDate, factEndDate,
planStartDate, planStartDate,
planEndDate, planEndDate,
predictStartDate, predictStartDate,
predictEndDate, predictEndDate,
}) })
}, },
setIsLoading, setIsLoading,
'Не удалось высчитать дополнительные данные' 'Не удалось высчитать дополнительные данные'
), [operations, setIsLoading]) )
}, [operations, setIsLoading])
return ( return (
<> <>

View File

@ -23,17 +23,21 @@ export const NptTable = memo(({ operations }) => {
const [filteredNPT, setFilteredNPT] = useState([]) const [filteredNPT, setFilteredNPT] = useState([])
const [isTableLoading, setIsTableLoading] = useState(false) const [isTableLoading, setIsTableLoading] = useState(false)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []), invokeWebApiWrapperAsync(
setIsTableLoading, async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []),
'Не удалось получить список НПВ' setIsTableLoading,
), [operations]) 'Не удалось получить список НПВ'
)
}, [operations])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)), invokeWebApiWrapperAsync(
setIsTableLoading, async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)),
'Не удалось отфильтровать НПВ по времени' setIsTableLoading,
), [npt, filterValue]) 'Не удалось отфильтровать НПВ по времени'
)
}, [npt, filterValue])
return ( return (
<div className={'tvd-right'}> <div className={'tvd-right'}>

View File

@ -1,5 +1,5 @@
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { memo, useState, useRef, useEffect, useCallback, useContext, useMemo } from 'react' import { memo, useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'
import { Switch, Button } from 'antd' import { Switch, Button } from 'antd'
@ -16,11 +16,10 @@ import 'chartjs-adapter-moment'
import zoomPlugin from 'chartjs-plugin-zoom' import zoomPlugin from 'chartjs-plugin-zoom'
import ChartDataLabels from 'chartjs-plugin-datalabels' import ChartDataLabels from 'chartjs-plugin-datalabels'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime' import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
import { getOperations } from '@utils/functions'
import NptTable from './NptTable' import NptTable from './NptTable'
import NetGraphExport from './NetGraphExport' import NetGraphExport from './NetGraphExport'
@ -30,14 +29,14 @@ import '@styles/index.css'
import '@styles/tvd.less' import '@styles/tvd.less'
Chart.register( Chart.register(
TimeScale, TimeScale,
LinearScale, LinearScale,
LineController, LineController,
LineElement, LineElement,
PointElement, PointElement,
Legend, Legend,
ChartDataLabels, ChartDataLabels,
zoomPlugin zoomPlugin
) )
const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-' const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-'
@ -115,18 +114,18 @@ const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
borderDash, borderDash,
}) })
export const Tvd = memo(({ idWell: wellId, title, ...other }) => { const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const [chart, setChart] = useState() const [chart, setChart] = useState()
const [xLabel, setXLabel] = useState('day') const [xLabel, setXLabel] = useState('day')
const [operations, setOperations] = useState({}) const [operations, setOperations] = useState({})
const [tableVisible, setTableVisible] = useState(false) const [tableVisible, setTableVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWellContext = useContext(IdWellContext) const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext]) const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
const chartRef = useRef(null) const chartRef = useRef(null)
const history = useHistory() const navigate = useNavigate()
const onPointClick = useCallback((e) => { const onPointClick = useCallback((e) => {
const points = e?.chart?.tooltip?.dataPoints const points = e?.chart?.tooltip?.dataPoints
@ -137,15 +136,17 @@ export const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const datasetName = datasetId === 2 ? 'plan' : 'fact' const datasetName = datasetId === 2 ? 'plan' : 'fact'
const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',') const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',')
history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`) navigate(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
}, [idWell, history]) }, [idWell, navigate])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => setOperations(await getOperations(idWell)), invokeWebApiWrapperAsync(
setIsLoading, async () => setOperations(await getOperations(idWell)),
`Не удалось загрузить операции по скважине "${idWell}"`, setIsLoading,
'Получение списка опервций по скважине' `Не удалось загрузить операции по скважине "${idWell}"`,
), [idWell]) 'Получение списка опервций по скважине'
)
}, [idWell])
useEffect(() => { useEffect(() => {
const withoutNpt = [] const withoutNpt = []
@ -227,4 +228,8 @@ export const Tvd = memo(({ idWell: wellId, title, ...other }) => {
) )
}) })
export default Tvd export default wrapPrivateComponent(Tvd, {
requirements: [ 'OperationStat.get' ],
title: 'TVD',
route: 'tvd',
})

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, memo, useMemo, useContext } from 'react' import { useState, useEffect, useCallback, memo, useMemo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeSelectColumn, makeSelectColumn,
@ -11,15 +11,14 @@ import {
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api' import { DrillParamsService, WellOperationService } from '@api'
import { hasPermission } from '@utils/permissions' import { hasPermission, arrayOrDefault } from '@utils'
import { arrayOrDefault } from '@utils'
export const getColumns = async (idWell) => { export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell) let sectionTypes = await WellOperationService.getSectionTypes(idWell)
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({ sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
label: value, label: value,
value: parseInt(id), value: id,
})) }))
return [ return [
@ -41,7 +40,7 @@ export const WellDrillParams = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateParams = useCallback(async () => await invokeWebApiWrapperAsync( const updateParams = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
@ -55,10 +54,12 @@ export const WellDrillParams = memo(() => {
'Получение списка режимов бурения скважины' 'Получение списка режимов бурения скважины'
), [idWell]) ), [idWell])
useEffect(() => (async () => { useEffect(() => {
setColumns(await getColumns(idWell)) (async () => {
await updateParams() setColumns(await getColumns(idWell))
})(), [idWell, updateParams]) await updateParams()
})()
}, [idWell, updateParams])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: DrillParamsService, service: DrillParamsService,
@ -73,8 +74,8 @@ export const WellDrillParams = memo(() => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={params} dataSource={params}
tableName={'well_drill_params'} tableName={'well_drill_params'}

View File

@ -1,9 +1,9 @@
import moment from 'moment' import moment from 'moment'
import { Input } from 'antd' import { Input } from 'antd'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeColumn, makeColumn,
@ -18,11 +18,9 @@ import {
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, wrapPrivateComponent, hasPermission } from '@utils'
import { arrayOrDefault } from '@utils'
import { WellOperationService } from '@api' import { WellOperationService } from '@api'
const { TextArea } = Input const { TextArea } = Input
const basePageSize = 160 const basePageSize = 160
@ -54,13 +52,13 @@ const generateColumns = (showNpt = false, categories = [], sectionTypes = []) =>
makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }), makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }),
].filter(Boolean) ].filter(Boolean)
export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => { const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize }) const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize })
const [paginationTotal, setPaginationTotal] = useState(0) const [paginationTotal, setPaginationTotal] = useState(0)
const [operations, setOperations] = useState([]) const [operations, setOperations] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [categories, setCategories] = useState([]) const [categories, setCategories] = useState([])
const [sectionTypes, setSectionTypes] = useState([]) const [sectionTypes, setSectionTypes] = useState([])
@ -73,18 +71,20 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt)) return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt))
}, [location]) }, [location])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const categories = arrayOrDefault(await WellOperationService.getCategories(idWell)) async () => {
setCategories(categories.map((item) => ({ value: item.id, label: item.name }))) const categories = arrayOrDefault(await WellOperationService.getCategories(idWell))
setCategories(categories.map((item) => ({ value: item.id, label: item.name })))
const sectionTypes = Object.entries(await WellOperationService.getSectionTypes(idWell) ?? {}) const sectionTypes = Object.entries(await WellOperationService.getSectionTypes(idWell) ?? {})
setSectionTypes(sectionTypes.map(([id, label]) => ({ value: parseInt(id), label }))) setSectionTypes(sectionTypes.map(([id, label]) => ({ value: parseInt(id), label })))
}, },
setShowLoader, setShowLoader,
'Не удалось загрузить список операций по скважине', 'Не удалось загрузить список операций по скважине',
'Получение списка операций по скважине' 'Получение списка операций по скважине'
), [idWell]) )
}, [idWell])
const updateOperations = useCallback(() => invokeWebApiWrapperAsync( const updateOperations = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -92,7 +92,7 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const take = pageNumAndPageSize.pageSize const take = pageNumAndPageSize.pageSize
const paginatedOperations = await WellOperationService.getOperations(idWell, const paginatedOperations = await WellOperationService.getOperations(idWell,
idType, undefined, undefined, undefined, undefined, idType, undefined, undefined, undefined, undefined,
undefined, undefined, skip, take ) undefined, undefined, skip, take)
const operations = paginatedOperations?.items ?? [] const operations = paginatedOperations?.items ?? []
setOperations(operations) setOperations(operations)
const total = paginatedOperations.count?? paginatedOperations.items?.length ?? 0 const total = paginatedOperations.count?? paginatedOperations.items?.length ?? 0
@ -103,7 +103,9 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
'Получение списка операций по скважине' 'Получение списка операций по скважине'
), [idWell, idType, pageNumAndPageSize]) ), [idWell, idType, pageNumAndPageSize])
useEffect(updateOperations, [updateOperations]) useEffect(() => {
updateOperations()
}, [updateOperations])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: WellOperationService, service: WellOperationService,
@ -151,4 +153,20 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
) )
}) })
export default WellOperationsEditor export const WellOperationsEditorPlan = wrapPrivateComponent(
() => <WellOperationsEditor idType={0} tableName={'well_operations_plan'}/>,
{
requirements: [ 'WellOperation.get' ],
title: 'План',
route: 'plan',
}
)
export const WellOperationsEditorFact = wrapPrivateComponent(
() => <WellOperationsEditor idType={1} tableName={'well_operations_fact'}/>,
{
requirements: [ 'WellOperation.get' ],
title: 'Факт',
route: 'fact',
}
)

View File

@ -1,10 +1,10 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table, makeColumn, makeColumnsPlanFact, makeNumericRender } from '@components/Table' import { Table, makeColumn, makeColumnsPlanFact, makeNumericRender } from '@components/Table'
import { calcDuration } from '@utils/datetime' import { calcDuration } from '@utils'
import { OperationStatService } from '@api' import { OperationStatService } from '@api'
@ -25,40 +25,42 @@ export const WellSectionsStat = memo(() => {
const [sections, setSections] = useState([]) const [sections, setSections] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => { invokeWebApiWrapperAsync(
const sectionsResponse = await OperationStatService.getStatWell(idWell) async () => {
const sectionsResponse = await OperationStatService.getStatWell(idWell)
if(sectionsResponse?.sections){ if(sectionsResponse?.sections){
const sections = sectionsResponse.sections.map(s => ({ const sections = sectionsResponse.sections.map(s => ({
key: s.id, key: s.id,
sectionType: s.caption, sectionType: s.caption,
wellDepthPlan: s.plan?.wellDepthEnd, wellDepthPlan: s.plan?.wellDepthEnd,
durationPlan: calcDuration(s.plan?.start, s.plan?.end), durationPlan: calcDuration(s.plan?.start, s.plan?.end),
ropPlan: s.plan?.rop, ropPlan: s.plan?.rop,
routeSpeedPlan: s.plan?.routeSpeed, routeSpeedPlan: s.plan?.routeSpeed,
bhaUpSpeedPlan: s.plan?.bhaUpSpeed, bhaUpSpeedPlan: s.plan?.bhaUpSpeed,
bhaDownSpeedPlan: s.plan?.bhaDownSpeed, bhaDownSpeedPlan: s.plan?.bhaDownSpeed,
casingDownSpeedPlan: s.plan?.casingDownSpeed, casingDownSpeedPlan: s.plan?.casingDownSpeed,
wellDepthFact: s.fact?.wellDepthEnd, wellDepthFact: s.fact?.wellDepthEnd,
durationFact: calcDuration(s.fact?.start, s.fact?.end), durationFact: calcDuration(s.fact?.start, s.fact?.end),
ropFact: s.fact?.rop, ropFact: s.fact?.rop,
routeSpeedFact: s.fact?.routeSpeed, routeSpeedFact: s.fact?.routeSpeed,
bhaUpSpeedFact: s.fact?.bhaUpSpeed, bhaUpSpeedFact: s.fact?.bhaUpSpeed,
bhaDownSpeedFact: s.fact?.bhaDownSpeed, bhaDownSpeedFact: s.fact?.bhaDownSpeed,
casingDownSpeedFact: s.fact?.casingDownSpeed, casingDownSpeedFact: s.fact?.casingDownSpeed,
})) }))
sections.sort((a,b) => a.wellDepthPlan - b.wellDepthPlan) sections.sort((a,b) => a.wellDepthPlan - b.wellDepthPlan)
setSections(sections) setSections(sections)
} }
}, },
setShowLoader, setShowLoader,
`Не удалось получить статистику по секциям скважины "${idWell}"`, `Не удалось получить статистику по секциям скважины "${idWell}"`,
'Получение статистики по секциям скважины' 'Получение статистики по секциям скважины'
), [idWell]) )
}, [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

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