forked from ddrilling/asb_cloud_front
Merge branch 'dev'
This commit is contained in:
commit
43aec286b3
31938
package-lock.json
generated
31938
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
"rxjs": "^7.5.4",
|
||||||
"typescript": "^4.2.3",
|
"typescript": "^4.2.3",
|
||||||
"web-vitals": "^1.1.1"
|
"web-vitals": "^1.1.1"
|
||||||
},
|
},
|
||||||
@ -34,7 +35,7 @@
|
|||||||
"react_test": "react-scripts test",
|
"react_test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"proxy": "http://46.146.209.148:89/",
|
"proxy": "http://46.146.209.148:89",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
19
src/App.js
19
src/App.js
@ -6,21 +6,22 @@ import {
|
|||||||
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 { OpenAPI } from './services/api'
|
import { OpenAPI } from '@api'
|
||||||
import { getUserToken } from './utils/storage'
|
import { getUserToken } from '@utils/storage'
|
||||||
import { PrivateRoute } from './components/Private'
|
import { PrivateRoute } from '@components/Private'
|
||||||
|
|
||||||
import Main from './pages/Main'
|
import Main from '@pages/Main'
|
||||||
import Login from './pages/Login'
|
import Login from '@pages/Login'
|
||||||
import Register from './pages/Register'
|
import Register from '@pages/Register'
|
||||||
|
|
||||||
import './styles/App.less'
|
import '@styles/App.less'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
//OpenAPI.BASE = 'http://localhost:3000'
|
//OpenAPI.BASE = 'http://localhost:3000'
|
||||||
OpenAPI.TOKEN = async () => getUserToken()
|
OpenAPI.TOKEN = async () => getUserToken()
|
||||||
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
export const App = () => (
|
export const App = memo(() => (
|
||||||
<ConfigProvider locale={locale}>
|
<ConfigProvider locale={locale}>
|
||||||
<Router>
|
<Router>
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -36,6 +37,6 @@ export const App = () => (
|
|||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
)
|
))
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
22
src/components/DownloadLink.tsx
Normal file
22
src/components/DownloadLink.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Button, ButtonProps } from 'antd'
|
||||||
|
import { FileWordOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { FileInfoDto } from '@api'
|
||||||
|
import { downloadFile } from './factory'
|
||||||
|
|
||||||
|
export type DownloadLinkProps = ButtonProps & {
|
||||||
|
file?: FileInfoDto
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DownloadLink = memo<DownloadLinkProps>(({ file, name, ...other }) => (
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
icon={<FileWordOutlined />}
|
||||||
|
onClick={file && (() => downloadFile(file))}
|
||||||
|
{...other}
|
||||||
|
>{name ?? file?.name ?? '-'}</Button>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default DownloadLink
|
@ -1,5 +1,5 @@
|
|||||||
import { memo, ReactNode } from 'react'
|
import { memo, ReactNode } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { Button, Layout, LayoutProps } from 'antd'
|
import { Button, Layout, LayoutProps } from 'antd'
|
||||||
|
|
||||||
import PageHeader from '@components/PageHeader'
|
import PageHeader from '@components/PageHeader'
|
||||||
@ -8,17 +8,21 @@ export type AdminLayoutPortalProps = LayoutProps & {
|
|||||||
title?: ReactNode
|
title?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => (
|
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
<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={'/'}>Вернуться на сайт</Link>
|
<Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default AdminLayoutPortal
|
export default AdminLayoutPortal
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { HTMLAttributes } from 'react'
|
import { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
import Loader from './Loader'
|
import { Loader } from '@components/icons'
|
||||||
|
|
||||||
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
|
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
show?: boolean,
|
show?: boolean,
|
||||||
fade?: boolean,
|
fade?: boolean,
|
||||||
|
spinnerProps?: HTMLAttributes<HTMLDivElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ show, fade = true, children, ...other }) => (
|
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className, show, fade = true, children, spinnerProps, ...other }) => (
|
||||||
<div className={'loader-container'} {...other}>
|
<div className={`loader-container ${className}`} {...other}>
|
||||||
<div className={'loader-content'}>{children}</div>
|
<div className={'loader-content'}>{children}</div>
|
||||||
{show && fade && <div className={'loader-fade'}/>}
|
{show && fade && <div className={'loader-fade'}/>}
|
||||||
{show && <div className={'loader-overlay'}><Loader/></div>}
|
{show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { Layout } from 'antd'
|
import { Layout } from 'antd'
|
||||||
import { Link } from 'react-router-dom'
|
import { BasicProps } from 'antd/lib/layout/layout'
|
||||||
|
|
||||||
import logo from '@images/logo_32.png'
|
|
||||||
import { headerHeight } from '@utils'
|
import { headerHeight } from '@utils'
|
||||||
import { UserMenu } from './UserMenu'
|
import { UserMenu } from './UserMenu'
|
||||||
import { BasicProps } from 'antd/lib/layout/layout'
|
|
||||||
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
export type PageHeaderProps = BasicProps & {
|
export type PageHeaderProps = BasicProps & {
|
||||||
title?: string
|
title?: string
|
||||||
@ -13,17 +14,21 @@ export type PageHeaderProps = BasicProps & {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
|
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Header className={'header'} {...other}>
|
<Layout.Header className={'header'} {...other}>
|
||||||
<Link to={'/'} style={{ height: headerHeight }}>
|
<Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}>
|
||||||
<img src={logo} alt={'АСБ'} className={'logo'}/>
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
{children}
|
{children}
|
||||||
<h1 className={'title'}>{title}</h1>
|
<h1 className={'title'}>{title}</h1>
|
||||||
<UserMenu isAdmin={isAdmin} />
|
<UserMenu isAdmin={isAdmin} />
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
</Layout>
|
</Layout>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default PageHeader
|
export default PageHeader
|
||||||
|
50
src/components/Poprompt.tsx
Normal file
50
src/components/Poprompt.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { memo, ReactNode, useCallback, useState } from 'react'
|
||||||
|
import { Button, ButtonProps, Form, FormProps, Popover, PopoverProps } from 'antd'
|
||||||
|
|
||||||
|
export type PopromptProps = PopoverProps & {
|
||||||
|
children?: ReactNode
|
||||||
|
footer?: ReactNode
|
||||||
|
text?: string
|
||||||
|
onDone?: (values: any) => void
|
||||||
|
formProps?: FormProps
|
||||||
|
buttonProps?: ButtonProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, children, onDone, text, ...other }) => {
|
||||||
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const onFormFinish = useCallback((values: any) => {
|
||||||
|
setVisible(false)
|
||||||
|
onDone?.(values)
|
||||||
|
}, [onDone])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={(
|
||||||
|
<Form
|
||||||
|
onFinish={onFormFinish}
|
||||||
|
autoComplete={'off'}
|
||||||
|
{...formProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<Form.Item style={{ marginBottom: 0 }}>
|
||||||
|
{footer ?? (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button htmlType={'reset'} onClick={() => setVisible(false)}>Отмена</Button>
|
||||||
|
<Button type={'primary'} htmlType={'submit'} style={{ marginLeft: '8px' }}>ОК</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
trigger={'click'}
|
||||||
|
{...other}
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={(visible) => setVisible(visible)}
|
||||||
|
>
|
||||||
|
<Button {...buttonProps}>{text}</Button>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Poprompt
|
@ -1,13 +1,14 @@
|
|||||||
import React from 'react'
|
import { memo, ReactElement } from 'react'
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils/permissions'
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
|
|
||||||
export type PrivateContentProps = {
|
export type PrivateContentProps = {
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
children?: React.ReactElement<any, any>
|
children?: ReactElement<any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivateContent: React.FC<PrivateContentProps> = ({ absolutePath, children = null }) =>
|
export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
|
||||||
isURLAvailable(absolutePath) ? children : null
|
isURLAvailable(absolutePath) ? children : null
|
||||||
|
)
|
||||||
|
|
||||||
export default PrivateContent
|
export default PrivateContent
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Redirect, Route, RouteProps } from 'react-router-dom'
|
import { Redirect, Route, RouteProps, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { getUserId } from '@utils/storage'
|
import { getUserId } from '@utils/storage'
|
||||||
import { isURLAvailable } from '@utils/permissions'
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
@ -9,10 +9,17 @@ export type PrivateDefaultRouteProps = RouteProps & {
|
|||||||
elseRedirect?: string
|
elseRedirect?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
|
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
<Route {...other} path={'/'}>
|
<Route {...other} path={'/'}>
|
||||||
<Redirect to={{ pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login') }} />
|
<Redirect to={{
|
||||||
|
pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login'),
|
||||||
|
state: { from: location.pathname },
|
||||||
|
}} />
|
||||||
</Route>
|
</Route>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default PrivateDefaultRoute
|
export default PrivateDefaultRoute
|
||||||
|
@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
|
|||||||
import { memo, NamedExoticComponent } from 'react'
|
import { memo, NamedExoticComponent } from 'react'
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils/permissions'
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
export type PrivateMenuItemProps = MenuItemProps & {
|
export type PrivateMenuItemProps = MenuItemProps & {
|
||||||
root: string
|
root: string
|
||||||
@ -16,11 +16,14 @@ export type PrivateMenuLinkProps = MenuItemProps & {
|
|||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path, title, ...other }) => (
|
export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path, title, ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
return (
|
||||||
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
||||||
<Link to={join(root, path)}>{title}</Link>
|
<Link to={{ pathname: join(root, path), state: { from: location.pathname }}}>{title}</Link>
|
||||||
</PrivateMenuItem>
|
</PrivateMenuItem>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
||||||
Link: NamedExoticComponent<PrivateMenuLinkProps>
|
Link: NamedExoticComponent<PrivateMenuLinkProps>
|
||||||
|
@ -14,7 +14,7 @@ export type PrivateRouteProps = RouteProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultRedirect = (location?: Location<unknown>) => (
|
export const defaultRedirect = (location?: Location<unknown>) => (
|
||||||
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location } }} />
|
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location?.pathname } }} />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => {
|
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => {
|
||||||
|
62
src/components/Table/Columns/index.ts
Normal file
62
src/components/Table/Columns/index.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
import { Rule } from 'antd/lib/form'
|
||||||
|
import { ColumnProps } from 'antd/lib/table'
|
||||||
|
|
||||||
|
export {
|
||||||
|
RegExpIsFloat,
|
||||||
|
makeNumericRender,
|
||||||
|
makeNumericColumn,
|
||||||
|
makeNumericColumnOptions,
|
||||||
|
makeNumericColumnPlanFact,
|
||||||
|
makeNumericStartEnd,
|
||||||
|
makeNumericMinMax,
|
||||||
|
makeNumericAvgRange
|
||||||
|
} from './numeric'
|
||||||
|
export { makeColumnsPlanFact } from './plan_fact'
|
||||||
|
export { makeSelectColumn } from './select'
|
||||||
|
export { makeTagColumn, makeTagInput } from './tag'
|
||||||
|
export { makeFilterTextMatch, makeTextColumn } from './text'
|
||||||
|
export {
|
||||||
|
timezoneOptions,
|
||||||
|
TimezoneSelect,
|
||||||
|
makeTimezoneColumn,
|
||||||
|
makeTimezoneRenderer
|
||||||
|
} from './timezone'
|
||||||
|
|
||||||
|
export type { TagInputProps } from './tag'
|
||||||
|
|
||||||
|
export type DataType<T = any> = Record<string, T>
|
||||||
|
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
||||||
|
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
||||||
|
|
||||||
|
/*
|
||||||
|
other - объект с дополнительными свойствами колонки
|
||||||
|
поддерживаются все базовые свойства из описания https://ant.design/components/table/#Column
|
||||||
|
плю дополнительные для колонок EditableTable: */
|
||||||
|
export type columnPropsOther<T = any> = ColumnProps<T> & {
|
||||||
|
// редактируемая колонка
|
||||||
|
editable?: boolean
|
||||||
|
// react компонента редактора
|
||||||
|
input?: ReactNode
|
||||||
|
// значение может быть пустым
|
||||||
|
isRequired?: boolean
|
||||||
|
// css класс для <FormItem/>, если требуется
|
||||||
|
formItemClass?: string
|
||||||
|
// массив правил валидации значений https://ant.design/components/form/#Rule
|
||||||
|
formItemRules?: Rule[]
|
||||||
|
// дефолтное значение при добавлении новой строки
|
||||||
|
initialValue?: string | number
|
||||||
|
|
||||||
|
render?: RenderMethod<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeColumn = (title: ReactNode, key: string, other?: columnPropsOther) => ({
|
||||||
|
title: title,
|
||||||
|
key: key,
|
||||||
|
dataIndex: key,
|
||||||
|
...other,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeGroupColumn = (title: ReactNode, children: object[]) => ({ title, children })
|
||||||
|
|
||||||
|
export default makeColumn
|
111
src/components/Table/Columns/numeric.tsx
Normal file
111
src/components/Table/Columns/numeric.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { InputNumber } from 'antd'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { makeNumericSorter } from '../sorters'
|
||||||
|
import { columnPropsOther, makeGroupColumn, RenderMethod } from '.'
|
||||||
|
|
||||||
|
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
||||||
|
|
||||||
|
export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMethod<T> => (value) => {
|
||||||
|
let val = '-'
|
||||||
|
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
||||||
|
val = (fixed ?? null) !== null
|
||||||
|
? (+value).toFixed(fixed)
|
||||||
|
: (+value).toPrecision(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'text-align-r-container'}>
|
||||||
|
<span>{val}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string): columnPropsOther => ({
|
||||||
|
editable: true,
|
||||||
|
initialValue: 0,
|
||||||
|
width: 100,
|
||||||
|
sorter: sorterKey ? makeNumericSorter(sorterKey) : undefined,
|
||||||
|
formItemRules: [{
|
||||||
|
required: true,
|
||||||
|
message: 'Введите число',
|
||||||
|
pattern: RegExpIsFloat,
|
||||||
|
}],
|
||||||
|
render: makeNumericRender(fixed),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeNumericColumn = (
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
filters: object[],
|
||||||
|
filterDelegate: (key: string | number) => any,
|
||||||
|
renderDelegate: (_: any, row: object) => any,
|
||||||
|
width: string,
|
||||||
|
other?: columnPropsOther
|
||||||
|
) => ({
|
||||||
|
title: title,
|
||||||
|
dataIndex: dataIndex,
|
||||||
|
key: dataIndex,
|
||||||
|
filters: filters,
|
||||||
|
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
|
||||||
|
sorter: makeNumericSorter(dataIndex),
|
||||||
|
width: width,
|
||||||
|
input: <InputNumber style={{ width: '100%' }}/>,
|
||||||
|
render: renderDelegate ?? makeNumericRender(),
|
||||||
|
align: 'right',
|
||||||
|
...other
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeNumericColumnPlanFact = (
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
filters: object[],
|
||||||
|
filterDelegate: (key: string | number) => any,
|
||||||
|
renderDelegate: (_: any, row: object) => any,
|
||||||
|
width: string
|
||||||
|
) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width),
|
||||||
|
makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const makeNumericStartEnd = (
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
fixed: number,
|
||||||
|
filters: object[],
|
||||||
|
filterDelegate: (key: string | number) => any,
|
||||||
|
renderDelegate: (_: any, row: object) => any,
|
||||||
|
width: string,
|
||||||
|
) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')),
|
||||||
|
makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End'))
|
||||||
|
])
|
||||||
|
|
||||||
|
export const makeNumericMinMax = (
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
fixed: number,
|
||||||
|
filters: object[],
|
||||||
|
filterDelegate: (key: string | number) => any,
|
||||||
|
renderDelegate: (_: any, row: object) => any,
|
||||||
|
width: string,
|
||||||
|
) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
||||||
|
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const makeNumericAvgRange = (
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
fixed: number,
|
||||||
|
filters: object[],
|
||||||
|
filterDelegate: (key: string | number) => any,
|
||||||
|
renderDelegate: (_: any, row: object) => any,
|
||||||
|
width: string
|
||||||
|
) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
||||||
|
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
|
||||||
|
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
|
||||||
|
])
|
||||||
|
|
||||||
|
export default makeNumericColumn
|
38
src/components/Table/Columns/plan_fact.tsx
Normal file
38
src/components/Table/Columns/plan_fact.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
|
export const makeColumnsPlanFact = (
|
||||||
|
title: string | ReactNode,
|
||||||
|
key: string | string[],
|
||||||
|
columsOther?: columnPropsOther | [columnPropsOther, columnPropsOther],
|
||||||
|
gruopOther?: any
|
||||||
|
) => {
|
||||||
|
let keyPlanLocal: string
|
||||||
|
let keyFactLocal: string
|
||||||
|
|
||||||
|
if (key instanceof Array) {
|
||||||
|
keyPlanLocal = key[0]
|
||||||
|
keyFactLocal = key[1]
|
||||||
|
} else {
|
||||||
|
keyPlanLocal = key + 'Plan'
|
||||||
|
keyFactLocal = key + 'Fact'
|
||||||
|
}
|
||||||
|
|
||||||
|
let columsOtherLocal : any[2]
|
||||||
|
if (columsOther instanceof Array)
|
||||||
|
columsOtherLocal = [columsOther[0], columsOther[1]]
|
||||||
|
else
|
||||||
|
columsOtherLocal = [columsOther, columsOther]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
...gruopOther,
|
||||||
|
children: [
|
||||||
|
makeColumn('план', keyPlanLocal, columsOtherLocal[0]),
|
||||||
|
makeColumn('факт', keyFactLocal, columsOtherLocal[1]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeColumnsPlanFact
|
22
src/components/Table/Columns/select.tsx
Normal file
22
src/components/Table/Columns/select.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Select, SelectProps } from 'antd'
|
||||||
|
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
|
||||||
|
|
||||||
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
|
export const makeSelectColumn = <T extends unknown = string>(
|
||||||
|
title: string,
|
||||||
|
dataIndex: string,
|
||||||
|
options: DefaultOptionType[],
|
||||||
|
defaultValue?: T,
|
||||||
|
other?: columnPropsOther,
|
||||||
|
selectOther?: SelectProps<SelectValue>
|
||||||
|
) => makeColumn(title, dataIndex, {
|
||||||
|
...other,
|
||||||
|
input: <Select options={options} {...selectOther}/>,
|
||||||
|
render: (value) => {
|
||||||
|
const item = options?.find(option => option?.value === value)
|
||||||
|
return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default makeSelectColumn
|
74
src/components/Table/Columns/tag.tsx
Normal file
74
src/components/Table/Columns/tag.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { Select, SelectProps, Tag } from 'antd'
|
||||||
|
import { DefaultOptionType, SelectValue } from 'antd/lib/select'
|
||||||
|
|
||||||
|
import { OmitExtends } from '@utils'
|
||||||
|
|
||||||
|
import { columnPropsOther, DataType, makeColumn } from '.'
|
||||||
|
|
||||||
|
export type TagInputProps<T extends DataType> = OmitExtends<{
|
||||||
|
options: T[],
|
||||||
|
value?: T[],
|
||||||
|
onChange?: (values: T[]) => void,
|
||||||
|
}, SelectProps<SelectValue>>
|
||||||
|
|
||||||
|
export const makeTagInput = <T extends DataType>(value_key: string, label_key: string) =>
|
||||||
|
memo<TagInputProps<T>>(({ options, value, onChange, ...other }) => {
|
||||||
|
const [selectOptions, setSelectOptions] = useState<DefaultOptionType[]>([])
|
||||||
|
const [selectedValue, setSelectedValue] = useState<SelectValue>([])
|
||||||
|
|
||||||
|
useEffect(() => setSelectOptions(options.map((elm) => ({
|
||||||
|
value: String(elm[value_key]),
|
||||||
|
label: elm[label_key],
|
||||||
|
}))), [options])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedValue(value?.map((elm) => String(elm[value_key])) ?? [])
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const onSelectChange = useCallback((rawValues?: SelectValue) => {
|
||||||
|
let values: any[] = []
|
||||||
|
if (typeof rawValues === 'string')
|
||||||
|
values = rawValues.split(',')
|
||||||
|
else if (Array.isArray(rawValues))
|
||||||
|
values = rawValues
|
||||||
|
|
||||||
|
const objectValues: T[] = values.reduce((out: T[], value: string) => {
|
||||||
|
const res = options.find((option) => String(option[value_key]) === String(value))
|
||||||
|
if (res) out.push(res)
|
||||||
|
return out
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
onChange?.(objectValues)
|
||||||
|
}, [onChange, options])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...other}
|
||||||
|
mode={'tags'}
|
||||||
|
options={selectOptions}
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={onSelectChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeTagColumn = <T extends DataType>(
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
options: T[],
|
||||||
|
value_key: keyof DataType,
|
||||||
|
label_key: keyof DataType,
|
||||||
|
other?: columnPropsOther,
|
||||||
|
tagOther?: TagInputProps<T>
|
||||||
|
) => {
|
||||||
|
const InputComponent = makeTagInput<T>(value_key, label_key)
|
||||||
|
|
||||||
|
return makeColumn(title, dataIndex, {
|
||||||
|
...other,
|
||||||
|
render: (item?: T[]) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm) ?? elm[label_key]}</Tag>) ?? '-',
|
||||||
|
input: <InputComponent {...tagOther} options={options} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeTagColumn
|
27
src/components/Table/Columns/text.tsx
Normal file
27
src/components/Table/Columns/text.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
||||||
|
import { makeStringSorter } from '../sorters'
|
||||||
|
|
||||||
|
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
|
||||||
|
(filterValue: T, dataItem: DataType<T>) => dataItem[key] === filterValue
|
||||||
|
|
||||||
|
export const makeTextColumn = <T extends unknown = any>(
|
||||||
|
title: ReactNode,
|
||||||
|
dataIndex: string,
|
||||||
|
filters: object[],
|
||||||
|
sorter?: SorterMethod<T>,
|
||||||
|
render?: RenderMethod<T>,
|
||||||
|
other?: columnPropsOther
|
||||||
|
) => ({
|
||||||
|
title: title,
|
||||||
|
dataIndex: dataIndex,
|
||||||
|
key: dataIndex,
|
||||||
|
filters: filters,
|
||||||
|
onFilter: filters ? makeFilterTextMatch(dataIndex) : null,
|
||||||
|
sorter: sorter ?? makeStringSorter(dataIndex),
|
||||||
|
render: render,
|
||||||
|
...other
|
||||||
|
})
|
||||||
|
|
||||||
|
export default makeTextColumn
|
75
src/components/Table/Columns/timezone.tsx
Normal file
75
src/components/Table/Columns/timezone.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { Select, SelectProps } from 'antd'
|
||||||
|
|
||||||
|
import { OmitExtends } from '@utils'
|
||||||
|
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils/datetime'
|
||||||
|
import { SimpleTimezoneDto } from '@api'
|
||||||
|
|
||||||
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
|
const makeTimezoneLabel = (id?: string | null, hours?: number) =>
|
||||||
|
`UTC${hours && hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${id ?? 'Неизвестно'}`
|
||||||
|
|
||||||
|
export const timezoneOptions = Object
|
||||||
|
.entries(rawTimezones)
|
||||||
|
.sort((a, b) => a[1] - b[1])
|
||||||
|
.map(([id, hours]) => ({
|
||||||
|
label: makeTimezoneLabel(id, hours),
|
||||||
|
value: id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const makeTimezoneRenderer = () => (timezone?: SimpleTimezoneDto) => {
|
||||||
|
if (!timezone) return 'UTC~?? :: Неизвестно'
|
||||||
|
const { hours, timezoneId } = timezone
|
||||||
|
return makeTimezoneLabel(timezoneId, hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimezoneSelectProps = OmitExtends<{
|
||||||
|
onChange?: ((value?: SimpleTimezoneDto | null) => void) | null
|
||||||
|
value?: SimpleTimezoneDto | null
|
||||||
|
defaultValue?: SimpleTimezoneDto | null
|
||||||
|
}, SelectProps<TimezoneId>>
|
||||||
|
|
||||||
|
export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, value, defaultValue, ...other }) => {
|
||||||
|
const [id, setId] = useState<TimezoneId | null>(null)
|
||||||
|
const [defaultTimezone, setDefaultTimezone] = useState<TimezoneId | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => setDefaultTimezone(defaultValue ? findTimezoneId(defaultValue) : null), [defaultValue])
|
||||||
|
useEffect(() => setId(value ? findTimezoneId(value) : null), [value])
|
||||||
|
|
||||||
|
const onValueChanged = useCallback((id: TimezoneId | null) => {
|
||||||
|
console.log(id)
|
||||||
|
onChange?.({
|
||||||
|
timezoneId: id,
|
||||||
|
hours: id ? rawTimezones[id] : 0,
|
||||||
|
isOverride: false,
|
||||||
|
})
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
|
return (<Select {...other} onChange={onValueChanged} value={id} defaultValue={defaultTimezone} />)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makeTimezoneColumn = (
|
||||||
|
title: ReactNode = 'Зона',
|
||||||
|
key: string = 'timezone',
|
||||||
|
defaultValue?: SimpleTimezoneDto,
|
||||||
|
allowClear: boolean = true,
|
||||||
|
other?: columnPropsOther,
|
||||||
|
selectOther?: TimezoneSelectProps
|
||||||
|
) => makeColumn(title, key, {
|
||||||
|
width: 100,
|
||||||
|
editable: true,
|
||||||
|
render: makeTimezoneRenderer(),
|
||||||
|
input: (
|
||||||
|
<TimezoneSelect
|
||||||
|
key={key}
|
||||||
|
allowClear={allowClear}
|
||||||
|
options={timezoneOptions}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
{...selectOther}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
...other
|
||||||
|
})
|
||||||
|
|
||||||
|
export default makeTimezoneColumn
|
35
src/components/Table/DateRangeWrapper.tsx
Normal file
35
src/components/Table/DateRangeWrapper.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { DatePicker, } from 'antd'
|
||||||
|
import moment, { Moment } from 'moment'
|
||||||
|
import { RangeValue } from 'rc-picker/lib/interface'
|
||||||
|
import { RangePickerSharedProps } from 'rc-picker/lib/RangePicker'
|
||||||
|
|
||||||
|
import { defaultFormat } from '@utils'
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker
|
||||||
|
|
||||||
|
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
|
||||||
|
value: RangeValue<Moment>,
|
||||||
|
isUTC?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeDates = (value: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => value && [
|
||||||
|
value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null,
|
||||||
|
value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, ...other }) => (
|
||||||
|
<RangePicker
|
||||||
|
showTime
|
||||||
|
allowClear={false}
|
||||||
|
format={defaultFormat}
|
||||||
|
defaultValue={[
|
||||||
|
moment().subtract(1, 'days').startOf('day'),
|
||||||
|
moment().startOf('day'),
|
||||||
|
]}
|
||||||
|
value={normalizeDates(value)}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default DateRangeWrapper
|
@ -1,7 +1,8 @@
|
|||||||
|
import { memo } 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 = {
|
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
|
||||||
editing?: boolean
|
editing?: boolean
|
||||||
dataIndex?: NamePath
|
dataIndex?: NamePath
|
||||||
input?: React.Component
|
input?: React.Component
|
||||||
@ -13,7 +14,7 @@ type EditableCellProps = {
|
|||||||
initialValue: any
|
initialValue: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditableCell = ({
|
export const EditableCell = memo<EditableCellProps>(({
|
||||||
editing,
|
editing,
|
||||||
dataIndex,
|
dataIndex,
|
||||||
input,
|
input,
|
||||||
@ -22,8 +23,9 @@ export const EditableCell = ({
|
|||||||
formItemRules,
|
formItemRules,
|
||||||
children,
|
children,
|
||||||
initialValue,
|
initialValue,
|
||||||
}: EditableCellProps) => (
|
...other
|
||||||
<td style={editing ? { padding: 0 } : undefined}>
|
}) => (
|
||||||
|
<td style={editing ? { padding: 0 } : undefined} {...other}>
|
||||||
{!editing ? children : (
|
{!editing ? children : (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={dataIndex}
|
name={dataIndex}
|
||||||
@ -39,4 +41,4 @@ export const EditableCell = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
)
|
))
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Form, Table, Button, Popconfirm } from 'antd'
|
import { memo, useCallback, useState, useEffect } from 'react'
|
||||||
|
import { Form, Button, Popconfirm } from 'antd'
|
||||||
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
|
||||||
|
import { Table } from '.'
|
||||||
import { EditableCell } from './EditableCell'
|
import { EditableCell } from './EditableCell'
|
||||||
|
|
||||||
const newRowKeyValue = 'newRow'
|
const newRowKeyValue = 'newRow'
|
||||||
@ -42,7 +43,7 @@ export const tryAddKeys = (items) => {
|
|||||||
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
|
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditableTable = ({
|
export const EditableTable = memo(({
|
||||||
columns,
|
columns,
|
||||||
dataSource,
|
dataSource,
|
||||||
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
|
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
|
||||||
@ -62,14 +63,14 @@ export const EditableTable = ({
|
|||||||
setData(tryAddKeys(dataSource))
|
setData(tryAddKeys(dataSource))
|
||||||
}, [dataSource])
|
}, [dataSource])
|
||||||
|
|
||||||
const isEditing = (record) => record?.key === editingKey
|
const isEditing = useCallback((record) => record?.key === editingKey, [editingKey])
|
||||||
|
|
||||||
const edit = (record) => {
|
const edit = useCallback((record) => {
|
||||||
form.setFieldsValue({...record})
|
form.setFieldsValue({...record})
|
||||||
setEditingKey(record.key)
|
setEditingKey(record.key)
|
||||||
}
|
}, [form])
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = useCallback(() => {
|
||||||
if (editingKey === newRowKeyValue) {
|
if (editingKey === newRowKeyValue) {
|
||||||
const newData = [...data]
|
const newData = [...data]
|
||||||
const index = newData.findIndex((item) => newRowKeyValue === item.key)
|
const index = newData.findIndex((item) => newRowKeyValue === item.key)
|
||||||
@ -77,9 +78,9 @@ export const EditableTable = ({
|
|||||||
setData(newData)
|
setData(newData)
|
||||||
}
|
}
|
||||||
setEditingKey('')
|
setEditingKey('')
|
||||||
}
|
}, [data, editingKey])
|
||||||
|
|
||||||
const addNewRow = async () => {
|
const addNewRow = useCallback(async () => {
|
||||||
let newRow = {
|
let newRow = {
|
||||||
...form.initialValues,
|
...form.initialValues,
|
||||||
key:newRowKeyValue
|
key:newRowKeyValue
|
||||||
@ -88,9 +89,9 @@ export const EditableTable = ({
|
|||||||
const newData = [newRow, ...data]
|
const newData = [newRow, ...data]
|
||||||
setData(newData)
|
setData(newData)
|
||||||
edit(newRow)
|
edit(newRow)
|
||||||
}
|
}, [data, edit, form.initialValues])
|
||||||
|
|
||||||
const save = async (record) => {
|
const save = useCallback(async (record) => {
|
||||||
try {
|
try {
|
||||||
const row = await form.validateFields()
|
const row = await form.validateFields()
|
||||||
const newData = [...data]
|
const newData = [...data]
|
||||||
@ -121,8 +122,7 @@ export const EditableTable = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (onChange)
|
onChange?.(newData)
|
||||||
onChange(newData)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('callback onChange fault:', err)
|
console.log('callback onChange fault:', err)
|
||||||
}
|
}
|
||||||
@ -130,9 +130,9 @@ export const EditableTable = ({
|
|||||||
} catch (errInfo) {
|
} catch (errInfo) {
|
||||||
console.log('Validate Failed:', errInfo)
|
console.log('Validate Failed:', errInfo)
|
||||||
}
|
}
|
||||||
}
|
}, [data, editingKey, form, onChange, onRowAdd, onRowEdit])
|
||||||
|
|
||||||
const deleteRow = (record) =>{
|
const deleteRow = useCallback((record) => {
|
||||||
const newData = [...data]
|
const newData = [...data]
|
||||||
const index = newData.findIndex((item) => record.key === item.key)
|
const index = newData.findIndex((item) => record.key === item.key)
|
||||||
|
|
||||||
@ -140,10 +140,8 @@ export const EditableTable = ({
|
|||||||
setData(newData)
|
setData(newData)
|
||||||
|
|
||||||
onRowDelete(record)
|
onRowDelete(record)
|
||||||
|
onChange?.(newData)
|
||||||
if (onChange)
|
}, [data, onChange, onRowDelete])
|
||||||
onChange(newData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const operationColumn = {
|
const operationColumn = {
|
||||||
width: buttonsWidth ?? 82,
|
width: buttonsWidth ?? 82,
|
||||||
@ -180,7 +178,7 @@ export const EditableTable = ({
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleColumn = (col) => {
|
const handleColumn = useCallback((col) => {
|
||||||
if (col.children)
|
if (col.children)
|
||||||
col.children = col.children.map(handleColumn)
|
col.children = col.children.map(handleColumn)
|
||||||
|
|
||||||
@ -197,13 +195,13 @@ export const EditableTable = ({
|
|||||||
input: col.input,
|
input: col.input,
|
||||||
isRequired: col.isRequired,
|
isRequired: col.isRequired,
|
||||||
title: col.title,
|
title: col.title,
|
||||||
dataType: col.dataType,
|
datatype: col.datatype,
|
||||||
formItemClass: col.formItemClass,
|
formItemClass: col.formItemClass,
|
||||||
formItemRules: col.formItemRules,
|
formItemRules: col.formItemRules,
|
||||||
initialValue: col.initialValue,
|
initialValue: col.initialValue,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}, [isEditing])
|
||||||
|
|
||||||
const mergedColumns = [...columns.map(handleColumn), operationColumn]
|
const mergedColumns = [...columns.map(handleColumn), operationColumn]
|
||||||
|
|
||||||
@ -221,4 +219,4 @@ export const EditableTable = ({
|
|||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
58
src/components/Table/Table.tsx
Normal file
58
src/components/Table/Table.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { ColumnGroupType, ColumnType } from 'antd/lib/table'
|
||||||
|
import { Table as RawTable, TableProps } from 'antd'
|
||||||
|
|
||||||
|
import { OmitExtends } from '@utils'
|
||||||
|
import { getTableSettings, setTableSettings } from '@utils/storage'
|
||||||
|
import { applySettings, ColumnSettings, TableSettings } from '@utils/table_settings'
|
||||||
|
|
||||||
|
import TableSettingsChanger from './TableSettingsChanger'
|
||||||
|
import { tryAddKeys } from './EditableTable'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
|
||||||
|
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
|
||||||
|
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, ColumnSettings>[]
|
||||||
|
|
||||||
|
export type TableContainer = TableProps<any> & {
|
||||||
|
columns: TableColumns
|
||||||
|
dataSource: any[]
|
||||||
|
tableName?: string
|
||||||
|
showSettingsChanger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Table = memo<TableContainer>(({ columns, dataSource, tableName, showSettingsChanger, ...other }) => {
|
||||||
|
const [newColumns, setNewColumns] = useState<TableColumns>([])
|
||||||
|
const [settings, setSettings] = useState<TableSettings>({})
|
||||||
|
|
||||||
|
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
|
||||||
|
if (tableName)
|
||||||
|
setTableSettings(tableName, settings)
|
||||||
|
setSettings(settings ?? {})
|
||||||
|
}, [tableName])
|
||||||
|
|
||||||
|
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
|
||||||
|
useEffect(() => setNewColumns(() => {
|
||||||
|
const newColumns = applySettings(columns, settings)
|
||||||
|
if (tableName && showSettingsChanger) {
|
||||||
|
const oldTitle = newColumns[0].title
|
||||||
|
newColumns[0].title = (props) => (
|
||||||
|
<div className={'first-column-title'}>
|
||||||
|
<TableSettingsChanger columns={columns} settings={settings} onChange={onSettingsChanged}/>
|
||||||
|
<div>{typeof oldTitle === 'function' ? oldTitle(props) : oldTitle}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return newColumns
|
||||||
|
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RawTable
|
||||||
|
columns={newColumns}
|
||||||
|
dataSource={tryAddKeys(dataSource)}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Table
|
96
src/components/Table/TableSettingsChanger.tsx
Normal file
96
src/components/Table/TableSettingsChanger.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { ColumnsType } from 'antd/lib/table'
|
||||||
|
import { Button, Modal, Switch, Table } from 'antd'
|
||||||
|
import { SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { ColumnSettings, makeSettings, mergeSettings, TableSettings } from '@utils/table_settings'
|
||||||
|
import { TableColumns } from './Table'
|
||||||
|
import { makeColumn } from '.'
|
||||||
|
|
||||||
|
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): ColumnSettings[] => {
|
||||||
|
const newSettings = mergeSettings(makeSettings(columns ?? []), settings ?? {})
|
||||||
|
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const unparseSettings = (columns: ColumnSettings[]): TableSettings =>
|
||||||
|
Object.fromEntries(columns.map((column) => [column.columnName, column]))
|
||||||
|
|
||||||
|
export type TableSettingsChangerProps = {
|
||||||
|
title?: string
|
||||||
|
columns?: TableColumns
|
||||||
|
settings?: TableSettings | null
|
||||||
|
onChange: (settings: TableSettings | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
|
||||||
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
|
const [newSettings, setNewSettings] = useState<ColumnSettings[]>(parseSettings(columns, settings))
|
||||||
|
const [tableColumns, setTableColumns] = useState<ColumnsType<ColumnSettings>>([])
|
||||||
|
|
||||||
|
const onVisibilityChange = useCallback((index: number, visible: boolean) => {
|
||||||
|
setNewSettings((oldSettings) => {
|
||||||
|
const newSettings = [...oldSettings]
|
||||||
|
newSettings[index].visible = visible
|
||||||
|
return newSettings
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toogleAll = useCallback((show: boolean) => {
|
||||||
|
setNewSettings((oldSettings) => oldSettings.map((column) => {
|
||||||
|
column.visible = show
|
||||||
|
return column
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableColumns([
|
||||||
|
makeColumn('Название', 'title'),
|
||||||
|
makeColumn(null, 'visible', {
|
||||||
|
title: () => (
|
||||||
|
<>
|
||||||
|
Показать
|
||||||
|
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
render: (visible: boolean, _?: ColumnSettings, index: number = NaN) => (
|
||||||
|
<Switch
|
||||||
|
checked={visible}
|
||||||
|
checkedChildren={'Отображён'}
|
||||||
|
unCheckedChildren={'Скрыт'}
|
||||||
|
onChange={(visible) => onVisibilityChange(index, visible)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}, [toogleAll, onVisibilityChange])
|
||||||
|
|
||||||
|
useEffect(() => setNewSettings(parseSettings(columns, settings)), [columns, settings])
|
||||||
|
|
||||||
|
const onModalOk = useCallback(() => {
|
||||||
|
onChange(unparseSettings(newSettings))
|
||||||
|
setVisible(false)
|
||||||
|
}, [newSettings, onChange])
|
||||||
|
|
||||||
|
const onModalCancel = useCallback(() => {
|
||||||
|
setNewSettings(parseSettings(columns, settings))
|
||||||
|
setVisible(false)
|
||||||
|
}, [columns, settings])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
visible={visible}
|
||||||
|
onCancel={onModalCancel}
|
||||||
|
onOk={onModalOk}
|
||||||
|
title={title ?? 'Настройка отображения таблицы'}
|
||||||
|
width={1000}
|
||||||
|
>
|
||||||
|
<Table columns={tableColumns} dataSource={newSettings} />
|
||||||
|
</Modal>
|
||||||
|
<Button size={'small'} style={{ position: 'absolute', left: 0, top: 0, opacity: .5 }} type={'link'} onClick={() => setVisible(true)} icon={<SettingOutlined />}/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TableSettingsChanger
|
@ -1,293 +1,44 @@
|
|||||||
import { memo, useEffect, useState, ReactNode } from 'react'
|
|
||||||
import { InputNumber, Select, Table as RawTable, Tag, SelectProps, TableProps } from 'antd'
|
|
||||||
import { SelectValue } from 'antd/lib/select'
|
|
||||||
import { OptionsType } from 'rc-select/lib/interface'
|
|
||||||
import { Rule } from 'rc-field-form/lib/interface'
|
|
||||||
|
|
||||||
import { tryAddKeys } from './EditableTable'
|
|
||||||
import { makeNumericSorter, makeStringSorter } from './sorters'
|
|
||||||
|
|
||||||
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
|
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
|
||||||
export { EditableTable, makeActionHandler } from './EditableTable'
|
export { EditableTable, makeActionHandler } from './EditableTable'
|
||||||
export { DatePickerWrapper } from './DatePickerWrapper'
|
export { DatePickerWrapper } from './DatePickerWrapper'
|
||||||
|
export { Table } from './Table'
|
||||||
|
export {
|
||||||
|
RegExpIsFloat,
|
||||||
|
timezoneOptions,
|
||||||
|
TimezoneSelect,
|
||||||
|
makeGroupColumn,
|
||||||
|
makeColumn,
|
||||||
|
makeColumnsPlanFact,
|
||||||
|
makeFilterTextMatch,
|
||||||
|
makeNumericRender,
|
||||||
|
makeNumericColumn,
|
||||||
|
makeNumericColumnOptions,
|
||||||
|
makeNumericColumnPlanFact,
|
||||||
|
makeNumericStartEnd,
|
||||||
|
makeNumericMinMax,
|
||||||
|
makeNumericAvgRange,
|
||||||
|
makeSelectColumn,
|
||||||
|
makeTagColumn,
|
||||||
|
makeTagInput,
|
||||||
|
makeTextColumn,
|
||||||
|
makeTimezoneColumn,
|
||||||
|
makeTimezoneRenderer,
|
||||||
|
} from './Columns'
|
||||||
|
|
||||||
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
export type {
|
||||||
|
DataType,
|
||||||
|
RenderMethod,
|
||||||
|
SorterMethod,
|
||||||
|
TagInputProps,
|
||||||
|
columnPropsOther,
|
||||||
|
} from './Columns'
|
||||||
|
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
|
||||||
|
|
||||||
export const defaultPagination = {
|
export const defaultPagination = {
|
||||||
defaultPageSize: 14,
|
defaultPageSize: 14,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeNumericRender = (fixed?: number) => (value: any, _: object): ReactNode => {
|
|
||||||
let val = '-'
|
|
||||||
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
|
||||||
val = (fixed ?? null) !== null
|
|
||||||
? (+value).toFixed(fixed)
|
|
||||||
: (+value).toPrecision(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'text-align-r-container'}>
|
|
||||||
<span>{val}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string) => ({
|
|
||||||
editable: true,
|
|
||||||
initialValue: 0,
|
|
||||||
width: 100,
|
|
||||||
sorter: sorterKey ? makeNumericSorter(sorterKey) : null,
|
|
||||||
formItemRules: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Введите число',
|
|
||||||
pattern: RegExpIsFloat,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
render: makeNumericRender(fixed),
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
other - объект с дополнительными свойствами колонки
|
|
||||||
поддерживаются все базовые свойства из описания https://ant.design/components/table/#Column
|
|
||||||
плю дополнительные для колонок EditableTable: */
|
|
||||||
interface columnPropsOther {
|
|
||||||
// редактируемая колонка
|
|
||||||
editable?: boolean
|
|
||||||
// react компонента редактора
|
|
||||||
input?: ReactNode
|
|
||||||
// значение может быть пустым
|
|
||||||
isRequired?: boolean
|
|
||||||
// css класс для <FormItem/>, если требуется
|
|
||||||
formItemClass?: string
|
|
||||||
// массив правил валидации значений https://ant.design/components/form/#Rule
|
|
||||||
formItemRules?: Rule[]
|
|
||||||
// дефолтное значение при добавлении новой строки
|
|
||||||
initialValue?: string | number
|
|
||||||
|
|
||||||
render?: (...attributes: any) => any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeColumn = (title: string | ReactNode, key: string, other?: columnPropsOther) => ({
|
|
||||||
title: title,
|
|
||||||
key: key,
|
|
||||||
dataIndex: key,
|
|
||||||
...other,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const makeColumnsPlanFact = (
|
|
||||||
title: string | ReactNode,
|
|
||||||
key: string | string[],
|
|
||||||
columsOther?: any | any[],
|
|
||||||
gruopOther?: any
|
|
||||||
) => {
|
|
||||||
let keyPlanLocal: string
|
|
||||||
let keyFactLocal: string
|
|
||||||
|
|
||||||
if (key instanceof Array) {
|
|
||||||
keyPlanLocal = key[0]
|
|
||||||
keyFactLocal = key[1]
|
|
||||||
} else {
|
|
||||||
keyPlanLocal = key + 'Plan'
|
|
||||||
keyFactLocal = key + 'Fact'
|
|
||||||
}
|
|
||||||
|
|
||||||
let columsOtherLocal : any[2]
|
|
||||||
if (columsOther instanceof Array)
|
|
||||||
columsOtherLocal = [columsOther[0], columsOther[1]]
|
|
||||||
else
|
|
||||||
columsOtherLocal = [columsOther, columsOther]
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: title,
|
|
||||||
...gruopOther,
|
|
||||||
children: [
|
|
||||||
makeColumn('план', keyPlanLocal, columsOtherLocal[0]),
|
|
||||||
makeColumn('факт', keyFactLocal, columsOtherLocal[1]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeFilterTextMatch = (key: string | number) => (
|
|
||||||
(filterValue: string | number, dataItem: any) => dataItem[key] === filterValue
|
|
||||||
)
|
|
||||||
|
|
||||||
export const makeGroupColumn = (title: string, children: object[]) => ({ title, children })
|
|
||||||
|
|
||||||
export const makeTextColumn = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
filters: object[],
|
|
||||||
sorter?: (key: string) => any,
|
|
||||||
render?: any,
|
|
||||||
other?: any
|
|
||||||
) => ({
|
|
||||||
title: title,
|
|
||||||
dataIndex: dataIndex,
|
|
||||||
key: dataIndex,
|
|
||||||
filters: filters,
|
|
||||||
onFilter: filters ? makeFilterTextMatch(dataIndex) : null,
|
|
||||||
sorter: sorter ?? makeStringSorter(dataIndex),
|
|
||||||
render: render,
|
|
||||||
...other
|
|
||||||
})
|
|
||||||
|
|
||||||
export const makeNumericColumn = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string,
|
|
||||||
other?: columnPropsOther
|
|
||||||
) => ({
|
|
||||||
title: title,
|
|
||||||
dataIndex: dataIndex,
|
|
||||||
key: dataIndex,
|
|
||||||
filters: filters,
|
|
||||||
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
|
|
||||||
sorter: makeNumericSorter(dataIndex),
|
|
||||||
width: width,
|
|
||||||
input: <InputNumber style={{ width: '100%' }}/>,
|
|
||||||
render: renderDelegate ?? makeNumericRender(),
|
|
||||||
align: 'right',
|
|
||||||
...other
|
|
||||||
})
|
|
||||||
|
|
||||||
export const makeNumericColumnPlanFact = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string
|
|
||||||
) => makeGroupColumn(title, [
|
|
||||||
makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width),
|
|
||||||
makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width),
|
|
||||||
])
|
|
||||||
|
|
||||||
export const makeNumericStartEnd = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
fixed: number,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string,
|
|
||||||
) => makeGroupColumn(title, [
|
|
||||||
makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')),
|
|
||||||
makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End'))
|
|
||||||
])
|
|
||||||
|
|
||||||
export const makeNumericMinMax = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
fixed: number,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string,
|
|
||||||
) => makeGroupColumn(title, [
|
|
||||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
|
||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
|
||||||
])
|
|
||||||
|
|
||||||
export const makeNumericAvgRange = (
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
fixed: number,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string
|
|
||||||
) => makeGroupColumn(title, [
|
|
||||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
|
||||||
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
|
|
||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
|
|
||||||
])
|
|
||||||
|
|
||||||
export const makeSelectColumn = <T extends unknown = string>(
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
options: OptionsType,
|
|
||||||
defaultValue?: T,
|
|
||||||
other?: columnPropsOther,
|
|
||||||
selectOther?: SelectProps<SelectValue>
|
|
||||||
) => makeColumn(title, dataIndex, {
|
|
||||||
...other,
|
|
||||||
input: <Select options={options} {...selectOther}/>,
|
|
||||||
render: (value) => {
|
|
||||||
const item = options?.find(option => option?.value === value)
|
|
||||||
return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeTagInput = <T extends Record<string, any>>(value_key: string, label_key: string) => memo<{
|
|
||||||
options: T[],
|
|
||||||
value?: T[],
|
|
||||||
onChange?: (values: T[]) => void,
|
|
||||||
other?: SelectProps<SelectValue>,
|
|
||||||
}>(({ options, value, onChange, other }) => {
|
|
||||||
const [selectOptions, setSelectOptions] = useState<OptionsType>([])
|
|
||||||
const [selectedValue, setSelectedValue] = useState<SelectValue>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectOptions(options.map((elm) => ({
|
|
||||||
value: String(elm[value_key]),
|
|
||||||
label: elm[label_key],
|
|
||||||
})))
|
|
||||||
}, [options])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedValue(value?.map((elm) => String(elm[value_key])) ?? [])
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
const onSelectChange = (rawValues?: SelectValue) => {
|
|
||||||
let values: any[] = []
|
|
||||||
if (typeof rawValues === 'string')
|
|
||||||
values = rawValues.split(',')
|
|
||||||
else if (Array.isArray(rawValues))
|
|
||||||
values = rawValues
|
|
||||||
|
|
||||||
const objectValues: T[] = values.reduce((out: T[], value: string) => {
|
|
||||||
const res = options.find((option) => String(option[value_key]) === String(value))
|
|
||||||
if (res) out.push(res)
|
|
||||||
return out
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
onChange?.(objectValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
{...other}
|
|
||||||
mode={'tags'}
|
|
||||||
options={selectOptions}
|
|
||||||
value={selectedValue}
|
|
||||||
onChange={onSelectChange}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const makeTagColumn = <T extends Record<string, any>>(
|
|
||||||
title: string,
|
|
||||||
dataIndex: string,
|
|
||||||
options: T[],
|
|
||||||
value_key: string,
|
|
||||||
label_key: string,
|
|
||||||
other?: columnPropsOther,
|
|
||||||
tagOther?: SelectProps<SelectValue>
|
|
||||||
) => {
|
|
||||||
const InputComponent = makeTagInput<T>(value_key, label_key)
|
|
||||||
|
|
||||||
return makeColumn(title, dataIndex, {
|
|
||||||
...other,
|
|
||||||
render: (item?: T[]) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm) ?? elm[label_key]}</Tag>) ?? '-',
|
|
||||||
input: <InputComponent options={options} other={tagOther} />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaginationContainer = {
|
type PaginationContainer = {
|
||||||
skip?: number
|
skip?: number
|
||||||
take?: number
|
take?: number
|
||||||
@ -301,12 +52,3 @@ export const makePaginationObject = (сontainer: PaginationContainer, ...other:
|
|||||||
total: сontainer.count ?? сontainer.items?.length ?? 0,
|
total: сontainer.count ?? сontainer.items?.length ?? 0,
|
||||||
current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1))
|
current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1))
|
||||||
})
|
})
|
||||||
|
|
||||||
export type TableContainer = TableProps<any> & {
|
|
||||||
dataSource: any[]
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Table = ({dataSource, children, ...other}: TableContainer) => (
|
|
||||||
<RawTable dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable>
|
|
||||||
)
|
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
export const makeNumericSorter = (key: string) => (a: any, b: any) => Number(a[key]) - Number(b[key])
|
import { isRawDate } from '@utils'
|
||||||
|
|
||||||
export const makeStringSorter = (key: string) => (a: any, b: any) => {
|
import { DataType } from './Columns'
|
||||||
|
|
||||||
|
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
||||||
|
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
||||||
|
|
||||||
|
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
|
||||||
if (!a && !b) return 0
|
if (!a && !b) return 0
|
||||||
if (!a) return 1
|
if (!a) return 1
|
||||||
if (!b) return -1
|
if (!b) return -1
|
||||||
|
|
||||||
return ('' + a[key]).localeCompare(b[key])
|
return String(a[key]).localeCompare(String(b[key]))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeDateSorter = (key: string) => (a: any, b: any) => {
|
export const makeDateSorter = <T extends unknown>(key: keyof DataType<T>) => (a: DataType<T>, b: DataType<T>) => {
|
||||||
const date = new Date(a[key])
|
const adate = a[key]
|
||||||
|
const bdate = b[key]
|
||||||
if (Number.isNaN(date.getTime()))
|
if (!isRawDate(adate) || !isRawDate(bdate))
|
||||||
throw new Error('Date column contains not date formatted string(s)')
|
throw new Error('Date column contains not date formatted string(s)')
|
||||||
|
|
||||||
return date.getTime() - new Date(b[key]).getTime()
|
const date = new Date(adate)
|
||||||
|
|
||||||
|
return date.getTime() - new Date(bdate).getTime()
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useCallback, useState } from 'react'
|
||||||
import { Upload, Button } from 'antd'
|
import { Upload, Button } from 'antd'
|
||||||
import { UploadOutlined } from '@ant-design/icons'
|
import { UploadOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { upload } from './factory'
|
import { upload } from './factory'
|
||||||
import { ErrorFetch } from './ErrorFetch'
|
import { ErrorFetch } from './ErrorFetch'
|
||||||
|
|
||||||
export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
export const UploadForm = memo(({ url, disabled, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
||||||
const [fileList, setfileList] = useState([])
|
const [fileList, setfileList] = useState([])
|
||||||
|
|
||||||
const handleFileSend = async () => {
|
const handleFileSend = useCallback(async () => {
|
||||||
onUploadStart?.()
|
onUploadStart?.()
|
||||||
try {
|
try {
|
||||||
const formDataLocal = new FormData()
|
const formDataLocal = new FormData()
|
||||||
@ -27,12 +27,14 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
|||||||
onUploadSuccess?.()
|
onUploadSuccess?.()
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
if(process.env.NODE_ENV === 'development')
|
||||||
|
console.error(error)
|
||||||
onUploadError?.(error)
|
onUploadError?.(error)
|
||||||
} finally {
|
} finally {
|
||||||
setfileList([])
|
setfileList([])
|
||||||
onUploadComplete?.()
|
onUploadComplete?.()
|
||||||
}
|
}
|
||||||
}
|
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
|
||||||
|
|
||||||
const isSendButtonEnabled = fileList.length > 0
|
const isSendButtonEnabled = fileList.length > 0
|
||||||
return(
|
return(
|
||||||
@ -40,10 +42,11 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
|||||||
<Upload
|
<Upload
|
||||||
name={'file'}
|
name={'file'}
|
||||||
accept={accept}
|
accept={accept}
|
||||||
|
disabled={disabled}
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
onChange={(props) => setfileList(props.fileList)}
|
onChange={(props) => setfileList(props.fileList)}
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined/>}>Загрузить файл</Button>
|
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
<Button
|
<Button
|
||||||
type={'primary'}
|
type={'primary'}
|
||||||
@ -56,3 +59,5 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export default UploadForm
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { memo, MouseEventHandler, useState } from 'react'
|
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
import { Button, Dropdown, DropDownProps, Menu } from 'antd'
|
import { Button, Dropdown, DropDownProps, Menu } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
@ -14,16 +14,17 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
|||||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
const onChangePasswordClick: MouseEventHandler = (e) => {
|
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
|
||||||
setIsModalVisible(true)
|
setIsModalVisible(true)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const onChangePasswordOk = () => {
|
const onChangePasswordOk = useCallback(() => {
|
||||||
setIsModalVisible(false)
|
setIsModalVisible(false)
|
||||||
history.push('/login')
|
history.push({ pathname: '/login', state: { from: location.pathname }})
|
||||||
}
|
}, [history, location])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -33,15 +34,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
|||||||
overlay={(
|
overlay={(
|
||||||
<Menu style={{ textAlign: 'right' }}>
|
<Menu style={{ textAlign: 'right' }}>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<PrivateMenuItemLink path={'/'} title={'Вернуться на сайт'}/>
|
<PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/>
|
||||||
) : (
|
) : (
|
||||||
<PrivateMenuItemLink path={'/admin'} title={'Панель администратора'}/>
|
<PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/>
|
||||||
)}
|
)}
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link>
|
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Link to={'/login'} onClick={removeUser}>Выход</Link>
|
<Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
77
src/components/WellSelector.jsx
Normal file
77
src/components/WellSelector.jsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Tag, TreeSelect } from 'antd'
|
||||||
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { hasPermission } from '@utils/permissions'
|
||||||
|
import { DepositService } from '@api'
|
||||||
|
|
||||||
|
export const getTreeData = async () => {
|
||||||
|
const deposits = await DepositService.getDeposits()
|
||||||
|
const wellsTree = deposits.map((deposit, dIdx) => ({
|
||||||
|
title: deposit.caption,
|
||||||
|
key: `0-${dIdx}`,
|
||||||
|
value: `0-${dIdx}`,
|
||||||
|
children: deposit.clusters.map((cluster, cIdx) => ({
|
||||||
|
title: cluster.caption,
|
||||||
|
key: `0-${dIdx}-${cIdx}`,
|
||||||
|
value: `0-${dIdx}-${cIdx}`,
|
||||||
|
children: cluster.wells.map(well => ({
|
||||||
|
title: well.caption,
|
||||||
|
key: well.id,
|
||||||
|
value: well.id,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return wellsTree
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTreeLabels = (treeData) => {
|
||||||
|
const labels = {}
|
||||||
|
treeData.forEach((deposit) =>
|
||||||
|
deposit?.children?.forEach((cluster) =>
|
||||||
|
cluster?.children?.forEach((well) => {
|
||||||
|
labels[well.value] = `${deposit.title}.${cluster.title}.${well.title}`
|
||||||
|
})))
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WellSelector = memo(({ idWell, value, onChange, treeData, treeLabels, ...other }) => {
|
||||||
|
const [wellsTree, setWellsTree] = useState([])
|
||||||
|
const [wellLabels, setWellLabels] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const wellsTree = treeData ?? await getTreeData()
|
||||||
|
const labels = treeLabels ?? getTreeLabels(wellsTree)
|
||||||
|
setWellsTree(wellsTree)
|
||||||
|
setWellLabels(labels)
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
'Не удалось загрузить список скважин',
|
||||||
|
'Получение списка скважин'
|
||||||
|
), [idWell, treeData, treeLabels])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeSelect
|
||||||
|
multiple
|
||||||
|
treeCheckable
|
||||||
|
showCheckedStrategy={TreeSelect.SHOW_CHILD}
|
||||||
|
treeDefaultExpandAll
|
||||||
|
treeData={wellsTree}
|
||||||
|
treeLine={{ showLeafIcon: false }}
|
||||||
|
size={'middle'}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={'Выберите скважины'}
|
||||||
|
tagRender={(props) => (
|
||||||
|
<Tag {...props}>{wellLabels[props.value] ?? props.label}</Tag>
|
||||||
|
)}
|
||||||
|
disabled={wellsTree.length <= 0 || !hasPermission('WellOperation.edit')}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default WellSelector
|
@ -1,7 +1,10 @@
|
|||||||
import { TreeSelect } from 'antd'
|
import { TreeSelect } from 'antd'
|
||||||
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
||||||
import { useState, useEffect, ReactNode } from 'react'
|
import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
|
||||||
import { useHistory, useRouteMatch } from 'react-router-dom'
|
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
|
||||||
|
import { LabelInValueType } from 'rc-select/lib/Select'
|
||||||
|
|
||||||
import { isRawDate } from '@utils'
|
import { isRawDate } from '@utils'
|
||||||
import LoaderPortal from './LoaderPortal'
|
import LoaderPortal from './LoaderPortal'
|
||||||
@ -54,11 +57,12 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellTreeSelector = () => {
|
export const WellTreeSelector = memo(({ ...other }) => {
|
||||||
const [wellsTree, setWellsTree] = useState<any[]>([]) // TODO: Исправить тип (необходимо разобраться с типом value rc-select)
|
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 history = useHistory()
|
||||||
|
const location = useLocation()
|
||||||
const routeMatch = useRouteMatch('/:route/:id')
|
const routeMatch = useRouteMatch('/:route/:id')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -100,14 +104,15 @@ export const WellTreeSelector = () => {
|
|||||||
setValue(getLabel(wellsTree, routeMatch?.url))
|
setValue(getLabel(wellsTree, routeMatch?.url))
|
||||||
}, [wellsTree, routeMatch])
|
}, [wellsTree, routeMatch])
|
||||||
|
|
||||||
const onChange = (value: string): void => {
|
const onChange = useCallback((value: string): void => {
|
||||||
if (wellsTree)
|
if (wellsTree)
|
||||||
setValue(getLabel(wellsTree, value))
|
setValue(getLabel(wellsTree, value))
|
||||||
}
|
}, [wellsTree])
|
||||||
|
|
||||||
const onSelect = (value: string): void => {
|
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
|
||||||
if (value) history.push(value)
|
if (['number', 'string'].includes(typeof value))
|
||||||
}
|
history.push({ pathname: String(value), state: { from: location.pathname }})
|
||||||
|
}, [history, location])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
@ -123,9 +128,10 @@ export const WellTreeSelector = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
style={{ width: '350px' }}
|
style={{ width: '350px' }}
|
||||||
|
{...other}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default WellTreeSelector
|
export default WellTreeSelector
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { memo, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
TimeScale,
|
TimeScale,
|
||||||
@ -178,7 +178,7 @@ export const timeParamsByInterval = (intervalSec: number): TimeParams => {
|
|||||||
return { unit, stepSize }
|
return { unit, stepSize }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams}) => {
|
export const ChartTimeBase = memo<ChartTimeBaseProps>(({ options, dataParams }) => {
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null)
|
const chartRef = useRef<HTMLCanvasElement>(null)
|
||||||
const [chart, setChart] = useState<any>()
|
const [chart, setChart] = useState<any>()
|
||||||
|
|
||||||
@ -217,4 +217,4 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams
|
|||||||
}, [chart, dataParams])
|
}, [chart, dataParams])
|
||||||
|
|
||||||
return(<canvas ref={chartRef} />)
|
return(<canvas ref={chartRef} />)
|
||||||
}
|
})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ChartOptions, Scriptable, ScriptableContext } from 'chart.js'
|
import { ChartOptions, Scriptable, ScriptableContext } from 'chart.js'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { makeDateSorter } from '../Table'
|
|
||||||
|
import { makeDateSorter } from '@components/Table'
|
||||||
import {
|
import {
|
||||||
ChartTimeBase,
|
ChartTimeBase,
|
||||||
ChartTimeData,
|
ChartTimeData,
|
||||||
@ -21,7 +22,8 @@ export type ColumnLineConfig = {
|
|||||||
dash?: Array<number>
|
dash?: Array<number>
|
||||||
borderColor?: string
|
borderColor?: string
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
borderWidth?: Scriptable<number, ScriptableContext<'radar'>>
|
borderWidth?: Scriptable<number, ScriptableContext<'line'>>
|
||||||
|
showDatalabels?: boolean
|
||||||
fill?: string
|
fill?: string
|
||||||
}
|
}
|
||||||
export type ColumnPostParsing = (data: ChartTimeDataParams) => void
|
export type ColumnPostParsing = (data: ChartTimeDataParams) => void
|
||||||
@ -44,7 +46,7 @@ const chartPluginsOptions: ChartOptions = {
|
|||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
color: '#000B',
|
color: '#000B',
|
||||||
display: context => (context.dataset.label === 'wellDepth') && 'auto',
|
display: context => !!context.dataset.label?.endsWith(' ') && 'auto',
|
||||||
formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`,
|
formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`,
|
||||||
padding: 6,
|
padding: 6,
|
||||||
align: 'left',
|
align: 'left',
|
||||||
@ -67,14 +69,14 @@ export const GetOrCreateDatasetByLineConfig = (data: ChartTimeData, lineConfig:
|
|||||||
?? GetRandomColor()
|
?? GetRandomColor()
|
||||||
|
|
||||||
dataset = {
|
dataset = {
|
||||||
label: lineConfig.label,
|
label: lineConfig.label?.trimEnd() + (lineConfig.showDatalabels ? ' ' : ''),
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: lineConfig.backgroundColor ?? color,
|
backgroundColor: lineConfig.backgroundColor ?? color,
|
||||||
borderColor: lineConfig.borderColor ?? color,
|
borderColor: lineConfig.borderColor ?? color,
|
||||||
borderWidth: lineConfig.borderWidth ?? 1,
|
borderWidth: lineConfig.borderWidth ?? 1,
|
||||||
borderDash: lineConfig.dash ?? [],
|
borderDash: lineConfig.dash ?? [],
|
||||||
showLine: lineConfig.showLine ?? !lineConfig.isShape,
|
showLine: lineConfig.showLine ?? !lineConfig.isShape,
|
||||||
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none')
|
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none'),
|
||||||
}
|
}
|
||||||
|
|
||||||
data.datasets.push(dataset)
|
data.datasets.push(dataset)
|
||||||
|
@ -36,7 +36,7 @@ type asyncFunction = (...args: any) => Promise<any|void>
|
|||||||
export const invokeWebApiWrapperAsync = async (
|
export const invokeWebApiWrapperAsync = async (
|
||||||
funcAsync: asyncFunction,
|
funcAsync: asyncFunction,
|
||||||
setShowLoader?: Dispatch<SetStateAction<boolean>>,
|
setShowLoader?: Dispatch<SetStateAction<boolean>>,
|
||||||
errorNotifyText?: (string | ((ex: unknown) => string)),
|
errorNotifyText?: ReactNode | ((ex: unknown) => ReactNode),
|
||||||
actionName?: string,
|
actionName?: string,
|
||||||
) => {
|
) => {
|
||||||
setShowLoader?.(true)
|
setShowLoader?.(true)
|
||||||
|
@ -3,3 +3,4 @@ export type { WellIconColors, WellIconProps, WellIconState } from './WellIcon'
|
|||||||
|
|
||||||
export { PointerIcon } from './PointerIcon'
|
export { PointerIcon } from './PointerIcon'
|
||||||
export { WellIcon } from './WellIcon'
|
export { WellIcon } from './WellIcon'
|
||||||
|
export { Loader } from './Loader'
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import { Tooltip, Tag, Typography, Popconfirm, Button } from 'antd'
|
|
||||||
import { memo } from 'react'
|
|
||||||
|
|
||||||
import { FileMarkDto } from '@api'
|
|
||||||
import { UserView } from './UserView'
|
|
||||||
|
|
||||||
const markTypes: { [id: number]: {color: string, text: string} } = {
|
|
||||||
0: {color: 'orange', text: 'неизвестно'},
|
|
||||||
1: {color: 'green', text: 'согласовано'},
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
export type MarkViewProps = {
|
|
||||||
mark: FileMarkDto
|
|
||||||
onDelete?: (e?: React.MouseEvent<HTMLElement, MouseEvent>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MarkView = memo<MarkViewProps>(({ mark, onDelete }) => {
|
|
||||||
const markType = markTypes[mark.idMarkType ?? 0] ?? markTypes[0]
|
|
||||||
return <Tooltip title={<UserView user={mark.user}/>}>
|
|
||||||
<Tag color={markType.color}>
|
|
||||||
<Text delete={mark?.isDeleted}>
|
|
||||||
{`${markType.text} ${new Date(mark.dateCreated ?? 0).toLocaleString()}`}
|
|
||||||
</Text>
|
|
||||||
{!mark?.isDeleted && (
|
|
||||||
<Popconfirm title='Отозвать согласование?' onConfirm={onDelete}>
|
|
||||||
<Button type='link'>x</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
})
|
|
@ -10,10 +10,12 @@ export type RoleViewProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RoleView = memo<RoleViewProps>(({ role }) => {
|
export const RoleView = memo<RoleViewProps>(({ role }) => {
|
||||||
|
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )
|
||||||
|
|
||||||
const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0
|
const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0
|
||||||
const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0
|
const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0
|
||||||
|
|
||||||
return role ? (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
overlayInnerStyle={{ width: '400px' }}
|
overlayInnerStyle={{ width: '400px' }}
|
||||||
title={
|
title={
|
||||||
@ -54,7 +56,5 @@ export const RoleView = memo<RoleViewProps>(({ role }) => {
|
|||||||
>
|
>
|
||||||
{role.caption}
|
{role.caption}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
|
||||||
<Tooltip title={'нет данных'}>-</Tooltip>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
export type { PermissionViewProps } from './PermissionView'
|
export type { PermissionViewProps } from './PermissionView'
|
||||||
export type { TelemetryViewProps } from './TelemetryView'
|
export type { TelemetryViewProps } from './TelemetryView'
|
||||||
export type { CompanyViewProps } from './CompanyView'
|
export type { CompanyViewProps } from './CompanyView'
|
||||||
export type { MarkViewProps } from './MarkView'
|
|
||||||
export type { RoleViewProps } from './RoleView'
|
export type { RoleViewProps } from './RoleView'
|
||||||
export type { UserViewProps } from './UserView'
|
export type { UserViewProps } from './UserView'
|
||||||
|
|
||||||
export { PermissionView } from './PermissionView'
|
export { PermissionView } from './PermissionView'
|
||||||
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
||||||
export { CompanyView } from './CompanyView'
|
export { CompanyView } from './CompanyView'
|
||||||
export { MarkView } from './MarkView'
|
|
||||||
export { RoleView } from './RoleView'
|
export { RoleView } from './RoleView'
|
||||||
export { UserView } from './UserView'
|
export { UserView } from './UserView'
|
7
src/images/Logo.tsx
Normal file
7
src/images/Logo.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => (
|
||||||
|
<img src={'/images/logo_32.png'} alt={'АСБ'} className={'logo'} {...props} />
|
||||||
|
))
|
||||||
|
|
||||||
|
export default Logo
|
@ -1,38 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1018.0595 0 8006.9226 3574.0189" version="1.1" fill="#2d2242">
|
|
||||||
<clipPath id="c">
|
|
||||||
<path d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
|
|
||||||
</clipPath>
|
|
||||||
<mask id="m" fill="#000">
|
|
||||||
<rect width="100%" height="100%" fill="#fff"/>
|
|
||||||
<polygon points="5166,1737 5061,1830 5089,1676"/>
|
|
||||||
<polygon points="5288,1737 5393,1830 5365,1676"/>
|
|
||||||
<polygon points="5224,1696 5285,1654 5172,1654"/>
|
|
||||||
<polygon points="5143,2007 5019,2062 5039,1952"/>
|
|
||||||
<polygon points="5310,2007 5435,2062 5415,1952"/>
|
|
||||||
<polygon points="5091,1894 5229,1962 5365,1894 5229,1783"/>
|
|
||||||
<polygon points="5052,2132 5232,2251 5412,2130 5229,2043"/>
|
|
||||||
<polygon points="5163,2297 4949,2445 4996,2184"/>
|
|
||||||
<polygon points="5292,2297 5505,2445 5458,2184"/>
|
|
||||||
<polygon points="5226,2337 5497,2523 4958,2523"/>
|
|
||||||
</mask>
|
|
||||||
<g fill="#e31e21">
|
|
||||||
<path d="M 1756,3564 H 1018 L 3236,2 3848,3 4452,0 4400,184 C 4637,66 4905,0 5189,0 5751,0 6253,261 6579,669 l -233,810 C 6213,964 5745,584 5189,584 c -528,0 -975,341 -1134,815 l -30,108 c -20,87 -31,178 -31,272 0,660 535,1195 1195,1195 318,0 607,-125 821,-327 l -220,764 c -185,88 -388,147 -601,147 -636,0 -1194,-334 -1508,-836 l -239,842 h -702 l 187,-595 H 2146 Z M 3082,2443 3703,446 2463,2444 Z"/>
|
|
||||||
<path d="m 7725,3574 c -392,-2 -748,-14 -1152,-2 L 5869,3559 6882,2 l 1790,1 -136,559 -1176,9 -121,462 h 836 c 570,93 953,697 950,1254 -3,656 -585,1291 -1300,1287 z m -995,-606 c 333,0 665,0 998,2 381,2 691,-335 693,-686 1,-291 -206,-632 -510,-673 h -824 z"/>
|
|
||||||
</g>
|
|
||||||
<g mask="url(#m)">
|
|
||||||
<polygon points="5347,1437 5105,1437 5105,1315 5347,1315"/>
|
|
||||||
<polygon points="5455,1555 4992,1555 4992,1469 5455,1469"/>
|
|
||||||
<polygon points="5597,2523 4860,2523 5027,1587 5419,1587"/>
|
|
||||||
</g>
|
|
||||||
<polygon points="5246,2523 5200,2523 5200,1735 5246,1735"/>
|
|
||||||
<g clip-path="url(#c)">
|
|
||||||
<g fill="#9d9e9e">
|
|
||||||
<path d="m 5136,177 c -688,66 -1152,378 -1415,911 l 1475,-196 783,591 z"/>
|
|
||||||
<path d="M 6684,1229 C 6401,599 5957,260 5367,182 l 659,1333 -308,931 z"/>
|
|
||||||
</g>
|
|
||||||
<path d="m 6189,3044 c 509,-466 692,-994 581,-1579 l -1059,1044 -981,-1 z"/>
|
|
||||||
<path d="m 4267,3105 c 598,345 1157,360 1681,78 L 4633,2488 4337,1552 Z"/>
|
|
||||||
<path d="m 3626,1346 c -142,676 17,1212 447,1622 l 253,-1466 798,-571 z"/>
|
|
||||||
</g>
|
|
||||||
<path fill="none" d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.6 KiB |
20
src/index.js
20
src/index.js
@ -1,17 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom'
|
||||||
import './styles/index.css';
|
import App from './App'
|
||||||
import App from './App';
|
import reportWebVitals from './reportWebVitals'
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
import '@styles/index.css'
|
||||||
|
|
||||||
|
ReactDOM.render((
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>
|
||||||
document.getElementById('root')
|
), document.getElementById('root'))
|
||||||
);
|
|
||||||
|
|
||||||
// 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))
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals();
|
reportWebVitals()
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export const AccessDenied = memo(() => (
|
export const AccessDenied = memo(() => (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100vw',
|
width: '100vw',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}>
|
}}>
|
||||||
Доступ запрещён
|
<h2>Доступ запрещён</h2>
|
||||||
|
<Link to={'/login'}>На страницу входа</Link>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
@ -6,7 +6,8 @@ import {
|
|||||||
makeSelectColumn,
|
makeSelectColumn,
|
||||||
makeActionHandler,
|
makeActionHandler,
|
||||||
makeStringSorter,
|
makeStringSorter,
|
||||||
defaultPagination
|
defaultPagination,
|
||||||
|
makeTimezoneColumn
|
||||||
} 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'
|
||||||
@ -17,7 +18,7 @@ import { hasPermission } from '@utils/permissions'
|
|||||||
|
|
||||||
import { coordsFixed } from './DepositController'
|
import { coordsFixed } from './DepositController'
|
||||||
|
|
||||||
export const ClusterController = () => {
|
export 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)
|
||||||
@ -36,9 +37,10 @@ export const ClusterController = () => {
|
|||||||
}),
|
}),
|
||||||
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 }),
|
||||||
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
]
|
]
|
||||||
|
|
||||||
const updateTable = () => invokeWebApiWrapperAsync(
|
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const clusters = await AdminClusterService.getAll()
|
const clusters = await AdminClusterService.getAll()
|
||||||
setClusters(arrayOrDefault(clusters))
|
setClusters(arrayOrDefault(clusters))
|
||||||
@ -46,7 +48,7 @@ export const ClusterController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список кустов`,
|
`Не удалось загрузить список кустов`,
|
||||||
'Получение списка кустов'
|
'Получение списка кустов'
|
||||||
)
|
), [])
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -59,7 +61,7 @@ export const ClusterController = () => {
|
|||||||
'Получение списка месторождений'
|
'Получение списка месторождений'
|
||||||
), [])
|
), [])
|
||||||
|
|
||||||
useEffect(updateTable, [])
|
useEffect(updateTable, [updateTable])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminClusterService,
|
service: AdminClusterService,
|
||||||
@ -79,9 +81,10 @@ export const ClusterController = () => {
|
|||||||
onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')}
|
onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')}
|
||||||
onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')}
|
onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')}
|
||||||
onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')}
|
onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')}
|
||||||
|
tableName={'admin_cluster_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default ClusterController
|
export default ClusterController
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
@ -16,15 +16,15 @@ import { min1 } from '@utils/validationRules'
|
|||||||
import { hasPermission } from '@utils/permissions'
|
import { hasPermission } from '@utils/permissions'
|
||||||
|
|
||||||
|
|
||||||
export const CompanyController = () => {
|
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)
|
||||||
|
|
||||||
const updateTable = async () => {
|
const updateTable = useCallback(async () => {
|
||||||
const companies = await AdminCompanyService.getAll()
|
const companies = await AdminCompanyService.getAll()
|
||||||
setCompanies(arrayOrDefault(companies))
|
setCompanies(arrayOrDefault(companies))
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async() => {
|
async() => {
|
||||||
@ -51,7 +51,7 @@ export const CompanyController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список типов компаний`,
|
`Не удалось загрузить список типов компаний`,
|
||||||
'Получение списка типов команд'
|
'Получение списка типов команд'
|
||||||
), [])
|
), [updateTable])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminCompanyService,
|
service: AdminCompanyService,
|
||||||
@ -76,9 +76,10 @@ export const CompanyController = () => {
|
|||||||
onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')}
|
onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')}
|
||||||
onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')}
|
onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')}
|
||||||
onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')}
|
onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')}
|
||||||
|
tableName={'admin_company_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default CompanyController
|
export default CompanyController
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
@ -12,7 +12,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { AdminCompanyTypeService } from '@api'
|
import { AdminCompanyTypeService } from '@api'
|
||||||
import { arrayOrDefault } from '@utils'
|
import { arrayOrDefault } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
import { hasPermission } from '@asb/utils/permissions'
|
import { hasPermission } from '@utils/permissions'
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
makeColumn('Название', 'caption', {
|
makeColumn('Название', 'caption', {
|
||||||
@ -23,11 +23,11 @@ const columns = [
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const CompanyTypeController = () => {
|
export const CompanyTypeController = memo(() => {
|
||||||
const [companyTypes, setCompanyTypes] = useState([])
|
const [companyTypes, setCompanyTypes] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
|
||||||
const updateTable = () => invokeWebApiWrapperAsync(
|
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async() => {
|
async() => {
|
||||||
const companyTypes = await AdminCompanyTypeService.getAll()
|
const companyTypes = await AdminCompanyTypeService.getAll()
|
||||||
setCompanyTypes(arrayOrDefault(companyTypes))
|
setCompanyTypes(arrayOrDefault(companyTypes))
|
||||||
@ -35,9 +35,9 @@ export const CompanyTypeController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список типов компаний`,
|
`Не удалось загрузить список типов компаний`,
|
||||||
'Получение списка типов компаний'
|
'Получение списка типов компаний'
|
||||||
)
|
), [])
|
||||||
|
|
||||||
useEffect(updateTable, [])
|
useEffect(updateTable, [updateTable])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminCompanyTypeService,
|
service: AdminCompanyTypeService,
|
||||||
@ -57,9 +57,10 @@ export const CompanyTypeController = () => {
|
|||||||
onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')}
|
onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')}
|
||||||
onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')}
|
onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')}
|
||||||
onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')}
|
onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')}
|
||||||
|
tableName={'admin_company_type_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default CompanyTypeController
|
export default CompanyTypeController
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { EditableTable, makeColumn, makeActionHandler, defaultPagination } from '@components/Table'
|
import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table'
|
||||||
import { AdminDepositService } from '@api'
|
import { AdminDepositService } from '@api'
|
||||||
import { arrayOrDefault } from '@utils'
|
import { arrayOrDefault } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
@ -13,14 +13,15 @@ export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).to
|
|||||||
const depositColumns = [
|
const depositColumns = [
|
||||||
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
||||||
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 }),
|
||||||
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DepositController = () => {
|
export const DepositController = memo(() => {
|
||||||
const [deposits, setDeposits] = useState([])
|
const [deposits, setDeposits] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
|
||||||
const updateTable = () => invokeWebApiWrapperAsync(
|
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async() => {
|
async() => {
|
||||||
const deposits = await AdminDepositService.getAll()
|
const deposits = await AdminDepositService.getAll()
|
||||||
setDeposits(arrayOrDefault(deposits))
|
setDeposits(arrayOrDefault(deposits))
|
||||||
@ -28,9 +29,9 @@ export const DepositController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список месторождении`,
|
`Не удалось загрузить список месторождении`,
|
||||||
'Получение списка месторождений'
|
'Получение списка месторождений'
|
||||||
)
|
), [])
|
||||||
|
|
||||||
useEffect(updateTable, [])
|
useEffect(updateTable, [updateTable])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminDepositService,
|
service: AdminDepositService,
|
||||||
@ -50,9 +51,10 @@ export const DepositController = () => {
|
|||||||
onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')}
|
onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')}
|
||||||
onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')}
|
onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')}
|
||||||
onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')}
|
onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')}
|
||||||
|
tableName={'admin_deposit_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default DepositController
|
export default DepositController
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
@ -26,11 +26,11 @@ const columns = [
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const PermissionController = () => {
|
export const PermissionController = memo(() => {
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [permissions, setPermissions] = useState([])
|
const [permissions, setPermissions] = useState([])
|
||||||
|
|
||||||
const updateTable = async () => invokeWebApiWrapperAsync(
|
const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const permission = await AdminPermissionService.getAll()
|
const permission = await AdminPermissionService.getAll()
|
||||||
setPermissions(arrayOrDefault(permission))
|
setPermissions(arrayOrDefault(permission))
|
||||||
@ -38,9 +38,9 @@ export const PermissionController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список прав`,
|
`Не удалось загрузить список прав`,
|
||||||
'Получение списка прав'
|
'Получение списка прав'
|
||||||
)
|
), [])
|
||||||
|
|
||||||
useEffect(() => updateTable(), [])
|
useEffect(() => updateTable(), [updateTable])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminPermissionService,
|
service: AdminPermissionService,
|
||||||
@ -60,9 +60,10 @@ export const PermissionController = () => {
|
|||||||
onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')}
|
onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')}
|
||||||
onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')}
|
onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')}
|
||||||
onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')}
|
onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')}
|
||||||
|
tableName={'admin_permission_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default PermissionController
|
export default PermissionController
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo, useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { PermissionView, RoleView } from '@components/views'
|
import { PermissionView, RoleView } from '@components/views'
|
||||||
@ -15,20 +15,21 @@ export const RoleController = memo(() => {
|
|||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [columns, setColumns] = useState([])
|
const [columns, setColumns] = useState([])
|
||||||
|
|
||||||
const loadRoles = async () => {
|
const loadRoles = useCallback(async () => {
|
||||||
const roles = await AdminUserRoleService.getAll()
|
const roles = await AdminUserRoleService.getAll()
|
||||||
setRoles(arrayOrDefault(roles))
|
setRoles(arrayOrDefault(roles))
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumns([
|
setColumns([
|
||||||
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
makeColumn('Название', 'caption', { width: 100, editable: true, formItemRules: min1 }),
|
||||||
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
|
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
|
||||||
width: 200,
|
width: 400,
|
||||||
editable: true,
|
editable: true,
|
||||||
render: (role) => <RoleView role={role} />
|
render: (role) => <RoleView role={role} />
|
||||||
}, { allowClear: true }),
|
}, { allowClear: true }),
|
||||||
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
|
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
|
||||||
|
width: 600,
|
||||||
editable: true,
|
editable: true,
|
||||||
render: (permission) => <PermissionView info={permission} />,
|
render: (permission) => <PermissionView info={permission} />,
|
||||||
}),
|
}),
|
||||||
@ -44,7 +45,7 @@ export const RoleController = memo(() => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список ролей`,
|
`Не удалось загрузить список ролей`,
|
||||||
'Получение списка ролей'
|
'Получение списка ролей'
|
||||||
), [])
|
), [loadRoles])
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminUserRoleService,
|
service: AdminUserRoleService,
|
||||||
@ -68,6 +69,7 @@ export const RoleController = memo(() => {
|
|||||||
onRowAdd={hasPermission('AdminUserRole.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление роли')}
|
onRowAdd={hasPermission('AdminUserRole.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление роли')}
|
||||||
onRowEdit={hasPermission('AdminUserRole.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование роли')}
|
onRowEdit={hasPermission('AdminUserRole.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование роли')}
|
||||||
onRowDelete={hasPermission('AdminUserRole.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление роли')}
|
onRowDelete={hasPermission('AdminUserRole.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление роли')}
|
||||||
|
tableName={'admin_role_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultPagination,
|
defaultPagination,
|
||||||
@ -29,7 +29,7 @@ const columns = [
|
|||||||
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
|
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const TelemetryController = () => {
|
export const TelemetryController = memo(() => {
|
||||||
const [telemetryData, setTelemetryData] = useState([])
|
const [telemetryData, setTelemetryData] = useState([])
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
@ -56,9 +56,10 @@ export const TelemetryController = () => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={telemetryData}
|
dataSource={telemetryData}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
|
tableName={'admin_telemetry_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default TelemetryController
|
export default TelemetryController
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, Tag } from 'antd'
|
import { Button, Input, Tag } from 'antd'
|
||||||
import { UserSwitchOutlined } from '@ant-design/icons'
|
import { UserSwitchOutlined } from '@ant-design/icons'
|
||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
@ -17,29 +18,71 @@ 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 } from '@utils/table'
|
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/table'
|
||||||
import { hasPermission } from '@utils/permissions'
|
import { hasPermission } from '@utils/permissions'
|
||||||
import { arrayOrDefault } from '@utils'
|
import { arrayOrDefault } from '@utils'
|
||||||
|
|
||||||
import RoleTag from './RoleTag'
|
import RoleTag from './RoleTag'
|
||||||
|
|
||||||
|
const SEARCH_TIMEOUT = 400
|
||||||
|
|
||||||
export const UserController = () => {
|
export const UserController = memo(() => {
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([])
|
||||||
|
const [filteredUsers, setFilteredUsers] = useState([])
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
const [columns, setColumns] = useState([])
|
const [columns, setColumns] = useState([])
|
||||||
const [selectedUser, setSelectedUser] = useState(null)
|
const [selectedUser, setSelectedUser] = useState(null)
|
||||||
|
const [subject, setSubject] = useState(null)
|
||||||
|
|
||||||
const additionalButtons = (record, editingKey) => (
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const filteredUsers = users.filter((user) => user && [
|
||||||
|
user.login ?? '',
|
||||||
|
user.name ?? '',
|
||||||
|
user.surname ?? '',
|
||||||
|
user.partonymic ?? '',
|
||||||
|
user.email ?? '',
|
||||||
|
user.phone ?? '',
|
||||||
|
user.position ?? '',
|
||||||
|
user.company?.caption ?? '',
|
||||||
|
].join(' ').toLowerCase().includes(searchValue))
|
||||||
|
setFilteredUsers(filteredUsers)
|
||||||
|
},
|
||||||
|
setIsSearching,
|
||||||
|
`Не удалось произвести поиск пользователей`
|
||||||
|
), [users, searchValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subject) {
|
||||||
|
const sub = new BehaviorSubject('')
|
||||||
|
setSubject(sub)
|
||||||
|
} else {
|
||||||
|
const observable = subject.pipe(
|
||||||
|
debounceTime(SEARCH_TIMEOUT),
|
||||||
|
map(s => s.trim().toLowerCase() ?? ''),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter(s => s.length <= 0 || s.length >= 2),
|
||||||
|
).subscribe(value => setSearchValue(value))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observable.unsubscribe()
|
||||||
|
subject.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [subject])
|
||||||
|
|
||||||
|
const additionalButtons = useCallback((record, editingKey) => (
|
||||||
<Button
|
<Button
|
||||||
icon={<UserSwitchOutlined />}
|
icon={<UserSwitchOutlined />}
|
||||||
onClick={() => setSelectedUser(record)}
|
onClick={() => setSelectedUser(record)}
|
||||||
title={'Сменить пароль'}
|
title={'Сменить пароль'}
|
||||||
disabled={editingKey !== ''}
|
disabled={editingKey !== ''}
|
||||||
/>
|
/>
|
||||||
)
|
), [])
|
||||||
|
|
||||||
const updateTable = () => invokeWebApiWrapperAsync(
|
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async() => {
|
async() => {
|
||||||
const users = await AdminUserService.getAll()
|
const users = await AdminUserService.getAll()
|
||||||
setUsers(arrayOrDefault(users))
|
setUsers(arrayOrDefault(users))
|
||||||
@ -47,7 +90,7 @@ export const UserController = () => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось загрузить список пользователей`,
|
`Не удалось загрузить список пользователей`,
|
||||||
'Получение списка пользователей'
|
'Получение списка пользователей'
|
||||||
)
|
), [])
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -61,7 +104,7 @@ export const UserController = () => {
|
|||||||
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 }))]
|
||||||
|
|
||||||
setColumns([
|
setColumns([
|
||||||
makeColumn('Логин', 'login', {
|
makeColumn('Логин', 'login', {
|
||||||
@ -124,6 +167,8 @@ export const UserController = () => {
|
|||||||
makeColumn('Роли', 'roleNames', {
|
makeColumn('Роли', 'roleNames', {
|
||||||
editable: true,
|
editable: true,
|
||||||
input: <RoleTag roles={roles} />,
|
input: <RoleTag roles={roles} />,
|
||||||
|
filters: roleFilters,
|
||||||
|
onFilter: makeArrayOnFilter('roleNames'),
|
||||||
render: (item) => item?.map((elm) => (
|
render: (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)} />
|
||||||
@ -148,20 +193,30 @@ export const UserController = () => {
|
|||||||
onComplete: updateTable,
|
onComplete: updateTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
placeholder={'Поиск пользователей'}
|
||||||
|
onChange={onSearchTextChange}
|
||||||
|
style={{ marginBottom: '15px' }}
|
||||||
|
loading={isSearching}
|
||||||
|
/>
|
||||||
<EditableTable
|
<EditableTable
|
||||||
size={'small'}
|
size={'small'}
|
||||||
bordered
|
bordered
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={users}
|
dataSource={filteredUsers}
|
||||||
onRowAdd={hasPermission('AdminUser.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление пользователя')}
|
onRowAdd={hasPermission('AdminUser.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление пользователя')}
|
||||||
onRowEdit={hasPermission('AdminUser.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование пользователя')}
|
onRowEdit={hasPermission('AdminUser.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование пользователя')}
|
||||||
onRowDelete={hasPermission('AdminUser.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление пользователя')}
|
onRowDelete={hasPermission('AdminUser.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление пользователя')}
|
||||||
additionalButtons={additionalButtons}
|
additionalButtons={additionalButtons}
|
||||||
buttonsWidth={120}
|
buttonsWidth={120}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
|
tableName={'admin_user_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
@ -172,6 +227,6 @@ export const UserController = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default UserController
|
export default UserController
|
||||||
|
@ -41,6 +41,7 @@ export const VisitLog = memo(() => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={logData}
|
dataSource={logData}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
|
tableName={'visit_log'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { CopyOutlined } from '@ant-design/icons'
|
import { CopyOutlined } from '@ant-design/icons'
|
||||||
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AdminClusterService,
|
AdminClusterService,
|
||||||
@ -17,24 +17,30 @@ import {
|
|||||||
makeNumericSorter,
|
makeNumericSorter,
|
||||||
makeTagColumn,
|
makeTagColumn,
|
||||||
defaultPagination,
|
defaultPagination,
|
||||||
|
makeTimezoneColumn,
|
||||||
} 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 { TelemetryView, CompanyView } from '@components/views'
|
import { TelemetryView, CompanyView } from '@components/views'
|
||||||
|
import { hasPermission } from '@utils/permissions'
|
||||||
import { arrayOrDefault } from '@utils'
|
import { arrayOrDefault } from '@utils'
|
||||||
|
|
||||||
import { coordsFixed } from '../DepositController'
|
import { coordsFixed } from '../DepositController'
|
||||||
import TelemetrySelect from './TelemetrySelect'
|
import TelemetrySelect from './TelemetrySelect'
|
||||||
|
|
||||||
import '@styles/admin.css'
|
import '@styles/admin.css'
|
||||||
import { hasPermission } from '@asb/utils/permissions'
|
|
||||||
|
|
||||||
const wellTypes = [
|
const wellTypes = [
|
||||||
{ value: 1, label: 'Наклонно-направленная' },
|
{ value: 1, label: 'Наклонно-направленная' },
|
||||||
{ value: 2, label: 'Горизонтальная' },
|
{ value: 2, label: 'Горизонтальная' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const WellController = () => {
|
const recordParser = (record) => ({
|
||||||
|
...record,
|
||||||
|
idTelemetry: record.telemetry?.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
export 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)
|
||||||
@ -53,14 +59,14 @@ export const WellController = () => {
|
|||||||
// TODO: Метод дубликации скважины
|
// TODO: Метод дубликации скважины
|
||||||
}
|
}
|
||||||
|
|
||||||
const addititonalButtons = (record, editingKey) => (
|
const addititonalButtons = memo((record, editingKey) => (
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
title={'Дублировать скважину'}
|
title={'Дублировать скважину'}
|
||||||
disabled={(editingKey ?? '') !== ''}
|
disabled={(editingKey ?? '') !== ''}
|
||||||
onClick={() => duplicateWell(record)}
|
onClick={() => duplicateWell(record)}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -94,6 +100,7 @@ export const WellController = () => {
|
|||||||
render: (telemetry) => <TelemetryView telemetry={telemetry} />,
|
render: (telemetry) => <TelemetryView telemetry={telemetry} />,
|
||||||
input: <TelemetrySelect telemetry={telemetry}/>,
|
input: <TelemetrySelect telemetry={telemetry}/>,
|
||||||
}, ),
|
}, ),
|
||||||
|
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} />,
|
||||||
@ -107,11 +114,6 @@ export const WellController = () => {
|
|||||||
'Получение списка кустов'
|
'Получение списка кустов'
|
||||||
), [])
|
), [])
|
||||||
|
|
||||||
const recordParser = (record) => ({
|
|
||||||
...record,
|
|
||||||
idTelemetry: record.telemetry?.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handlerProps = {
|
const handlerProps = {
|
||||||
service: AdminWellService,
|
service: AdminWellService,
|
||||||
setLoader: setShowLoader,
|
setLoader: setShowLoader,
|
||||||
@ -132,9 +134,10 @@ export const WellController = () => {
|
|||||||
onRowDelete={hasPermission('AdminWell.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление скважины')}
|
onRowDelete={hasPermission('AdminWell.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление скважины')}
|
||||||
//additionalButtons={addititonalButtons}
|
//additionalButtons={addititonalButtons}
|
||||||
buttonsWidth={95}
|
buttonsWidth={95}
|
||||||
|
tableName={'admin_well_controller'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default WellController
|
export default WellController
|
||||||
|
217
src/pages/Analytics/Statistics.jsx
Normal file
217
src/pages/Analytics/Statistics.jsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { Table as RawTable, Typography } from 'antd'
|
||||||
|
import { Fragment, memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { WellSelector } from '@components/WellSelector'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
|
||||||
|
import { OperationStatService, WellOperationService } from '@api'
|
||||||
|
import { arrayOrDefault } from '@utils'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
import '@styles/statistics.less'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
const { Summary } = RawTable
|
||||||
|
const { Cell, Row } = Summary
|
||||||
|
|
||||||
|
const numericRender = makeNumericRender()
|
||||||
|
const speedNumericRender = (section) => numericRender(section?.speed)
|
||||||
|
|
||||||
|
export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100),
|
||||||
|
makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100),
|
||||||
|
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const defaultColumns = [
|
||||||
|
//makeTextColumn('Куст', 'cluster', null, null, null, { fixed: 'left', width: 100 }),
|
||||||
|
makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }),
|
||||||
|
]
|
||||||
|
|
||||||
|
const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: 200 }
|
||||||
|
const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2
|
||||||
|
|
||||||
|
const getWellData = async (wellsList) => {
|
||||||
|
const stats = arrayOrDefault(await OperationStatService.getWellsStat(wellsList))
|
||||||
|
const wellData = stats.map((well) => {
|
||||||
|
const stat = {
|
||||||
|
// cluster: null,
|
||||||
|
caption: well.caption,
|
||||||
|
}
|
||||||
|
|
||||||
|
well.sections?.forEach(({ id, fact }) => {
|
||||||
|
if (!fact) return
|
||||||
|
stat[`section_${id}`] = {
|
||||||
|
time: (+new Date(fact.end) - +new Date(fact.start)) / 3600_000,
|
||||||
|
depth: fact.wellDepthEnd - fact.wellDepthStart,
|
||||||
|
speed: fact.routeSpeed,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return stat
|
||||||
|
})
|
||||||
|
|
||||||
|
return wellData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Statistics = memo(({ idWell }) => {
|
||||||
|
const [sectionTypes, setSectionTypes] = useState([])
|
||||||
|
const [avgColumns, setAvgColumns] = useState(defaultColumns)
|
||||||
|
const [cmpColumns, setCmpColumns] = useState(defaultColumns)
|
||||||
|
const [isAvgTableLoading, setIsAvgTableLoading] = useState(false)
|
||||||
|
const [isCmpTableLoading, setIsCmpTableLoading] = useState(false)
|
||||||
|
const [isPageLoading, setIsPageLoading] = useState(false)
|
||||||
|
const [avgWells, setAvgWells] = useState([])
|
||||||
|
const [cmpWells, setCmpWells] = useState([])
|
||||||
|
const [avgData, setAvgData] = useState([])
|
||||||
|
const [cmpData, setCmpData] = useState([])
|
||||||
|
const [avgRow, setAvgRow] = useState({})
|
||||||
|
|
||||||
|
const cmpSpeedRender = useCallback((key) => (section) => {
|
||||||
|
let spanClass = ''
|
||||||
|
// Дополнительная проверка на "null" необходима, чтобы значение "0" не стало исключением
|
||||||
|
if ((avgRow[key]?.speed ?? null) !== null && (section?.speed ?? null) !== null) {
|
||||||
|
const avgSpeed = avgRow[key].speed - section.speed
|
||||||
|
if (avgSpeed < 0)
|
||||||
|
spanClass = 'high-efficienty'
|
||||||
|
else if (avgSpeed > 0)
|
||||||
|
spanClass = 'low-efficienty'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={spanClass}>
|
||||||
|
{numericRender(section?.speed)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}, [avgRow])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const types = await WellOperationService.getSectionTypes(idWell)
|
||||||
|
setSectionTypes(Object.entries(types))
|
||||||
|
setAvgColumns([
|
||||||
|
...defaultColumns,
|
||||||
|
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`)),
|
||||||
|
])
|
||||||
|
setCmpColumns([
|
||||||
|
...defaultColumns,
|
||||||
|
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`, {
|
||||||
|
speedRender: cmpSpeedRender(`section_${id}`)
|
||||||
|
}))
|
||||||
|
])
|
||||||
|
},
|
||||||
|
setIsPageLoading,
|
||||||
|
`Не удалось получить типы секции`,
|
||||||
|
`Получение списка возможных секций`,
|
||||||
|
), [idWell, 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++
|
||||||
|
}))
|
||||||
|
|
||||||
|
Object.values(avgRow).forEach((section) => section.speed /= section.count)
|
||||||
|
|
||||||
|
setAvgRow(avgRow)
|
||||||
|
},
|
||||||
|
setIsAvgTableLoading,
|
||||||
|
'Не удалось загрузить данные для расчёта средних значений',
|
||||||
|
), [avgWells])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const cmpData = await getWellData(cmpWells)
|
||||||
|
setCmpData(cmpData)
|
||||||
|
},
|
||||||
|
setIsCmpTableLoading,
|
||||||
|
'Не удалось получить скважины для сравнения',
|
||||||
|
), [cmpWells])
|
||||||
|
|
||||||
|
const getStatisticsAvgSummary = useCallback((data) => (
|
||||||
|
<Summary fixed={'bottom'}>
|
||||||
|
<Row>
|
||||||
|
<Cell index={0} colSpan={summaryColSpan}>
|
||||||
|
<Text>Итого:</Text>
|
||||||
|
</Cell>
|
||||||
|
{sectionTypes.map(([id, _], i) => (
|
||||||
|
<Fragment key={id ?? i}>
|
||||||
|
<Cell index={3 * i + summaryColSpan}>
|
||||||
|
<Text>{numericRender(avgRow[`section_${id}`]?.depth)}</Text>
|
||||||
|
</Cell>
|
||||||
|
<Cell index={3 * i + summaryColSpan + 1}>
|
||||||
|
<Text>{numericRender(avgRow[`section_${id}`]?.time)}</Text>
|
||||||
|
</Cell>
|
||||||
|
<Cell index={3 * i + summaryColSpan + 2}>
|
||||||
|
<Text>{numericRender(avgRow[`section_${id}`]?.speed)}</Text>
|
||||||
|
</Cell>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Summary>
|
||||||
|
), [avgRow, sectionTypes])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'statistics-page'}>
|
||||||
|
<LoaderPortal show={isPageLoading}>
|
||||||
|
<h2 style={{ padding: '10px' }}>Расчёт средних значений без Цифровой буровой</h2>
|
||||||
|
<div className={'average-table'}>
|
||||||
|
<div className={'well-selector'}>
|
||||||
|
<span className={'well-selector-label'}>Выберите скважины для расчёта средних значений:</span>
|
||||||
|
<WellSelector
|
||||||
|
idWell={idWell}
|
||||||
|
value={avgWells}
|
||||||
|
style={{ flex: 100 }}
|
||||||
|
onChange={setAvgWells}
|
||||||
|
placeholder={'Ничего не выбрано'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size={'small'}
|
||||||
|
pagination={false}
|
||||||
|
columns={avgColumns}
|
||||||
|
dataSource={avgData}
|
||||||
|
scroll={scrollSettings}
|
||||||
|
loading={isAvgTableLoading}
|
||||||
|
summary={getStatisticsAvgSummary}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'compare-table'}>
|
||||||
|
<div className={'well-selector'}>
|
||||||
|
<span className={'well-selector-label'}>Выберите скважины сравнения:</span>
|
||||||
|
<WellSelector
|
||||||
|
idWell={idWell}
|
||||||
|
value={cmpWells}
|
||||||
|
style={{ flex: 100 }}
|
||||||
|
onChange={setCmpWells}
|
||||||
|
placeholder={'Ничего не выбрано'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size={'small'}
|
||||||
|
pagination={false}
|
||||||
|
columns={cmpColumns}
|
||||||
|
dataSource={cmpData}
|
||||||
|
scroll={scrollSettings}
|
||||||
|
loading={isCmpTableLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Statistics
|
@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo, useCallback } 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, Popconfirm } from 'antd'
|
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd'
|
||||||
|
|
||||||
@ -9,15 +9,15 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
|
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
|
||||||
import { DrillParamsService, WellCompositeService } from '@api'
|
import { DrillParamsService, WellCompositeService } from '@api'
|
||||||
import { hasPermission } from '@utils/permissions'
|
import { hasPermission } from '@utils/permissions'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
calcAndUpdateStatsBySections,
|
calcAndUpdateStatsBySections,
|
||||||
makeFilterMinMaxFunction,
|
makeFilterMinMaxFunction,
|
||||||
getOperations
|
getOperations
|
||||||
} from '@pages/Cluster/functions'
|
} from '@utils/functions'
|
||||||
|
|
||||||
|
import { Tvd } from '@pages/WellOperations/Tvd'
|
||||||
|
import { getColumns } from '@pages/WellOperations/WellDrillParams'
|
||||||
import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
|
import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
|
||||||
import { Tvd } from '../Tvd'
|
|
||||||
import { getColumns } from '../WellDrillParams'
|
|
||||||
|
|
||||||
|
|
||||||
const filtersMinMax = [
|
const filtersMinMax = [
|
||||||
@ -42,6 +42,8 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
|||||||
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
|
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
|
||||||
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
|
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
|
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -125,7 +127,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
|||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
makeTextColumn('скв №', 'caption', null, null,
|
makeTextColumn('скв №', 'caption', null, null,
|
||||||
(text, item) => <Link to={`/well/${item?.id}`}>{text ?? '-'}</Link>
|
(text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
|
||||||
),
|
),
|
||||||
makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'),
|
makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'),
|
||||||
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
|
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
|
||||||
@ -187,7 +189,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onParamButtonClick = () => invokeWebApiWrapperAsync(
|
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
setIsParamsModalVisible(true)
|
setIsParamsModalVisible(true)
|
||||||
const params = await DrillParamsService.getCompositeAll(idWell)
|
const params = await DrillParamsService.getCompositeAll(idWell)
|
||||||
@ -196,9 +198,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
|||||||
setShowParamsLoader,
|
setShowParamsLoader,
|
||||||
`Не удалось загрузить список режимов для скважины "${idWell}"`,
|
`Не удалось загрузить список режимов для скважины "${idWell}"`,
|
||||||
'Получение списка режимов скважины'
|
'Получение списка режимов скважины'
|
||||||
)
|
), [idWell])
|
||||||
|
|
||||||
const onParamsAddClick = () => invokeWebApiWrapperAsync(
|
const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await DrillParamsService.save(idWell, params)
|
await DrillParamsService.save(idWell, params)
|
||||||
setIsParamsModalVisible(false)
|
setIsParamsModalVisible(false)
|
||||||
@ -206,7 +208,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось добавить режимы в список скважины "${idWell}"`,
|
`Не удалось добавить режимы в список скважины "${idWell}"`,
|
||||||
'Добавление режима скважины'
|
'Добавление режима скважины'
|
||||||
)
|
), [idWell, params])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -1,15 +1,14 @@
|
|||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
import { Switch, useParams } from 'react-router-dom'
|
import { Switch, useParams } from 'react-router-dom'
|
||||||
import { Col, Layout, Menu, Row, Tag, TreeSelect } from 'antd'
|
import { Col, Layout, Menu, Row } from 'antd'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DepositService,
|
|
||||||
OperationStatService,
|
OperationStatService,
|
||||||
WellCompositeService,
|
WellCompositeService,
|
||||||
} from '@api'
|
} from '@api'
|
||||||
import { arrayOrDefault } from '@utils'
|
import { arrayOrDefault } from '@utils'
|
||||||
import { hasPermission } from '@utils/permissions'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import WellSelector from '@components/WellSelector'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
|
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
|
||||||
|
|
||||||
@ -18,45 +17,17 @@ import { WellCompositeSections } from './WellCompositeSections'
|
|||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
export const WellCompositeEditor = memo(({ idWell }) => {
|
export const WellCompositeEditor = memo(({ idWell, rootPath }) => {
|
||||||
const rootPath = `/well/${idWell}/operations/composite`
|
|
||||||
const { tab } = useParams()
|
const { tab } = useParams()
|
||||||
|
|
||||||
const [wellsTree, setWellsTree] = useState([])
|
|
||||||
const [wellLabels, setWellLabels] = useState([])
|
|
||||||
const [statsWells, setStatsWells] = useState([])
|
const [statsWells, setStatsWells] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [showTabLoader, setShowTabLoader] = useState(false)
|
const [showTabLoader, setShowTabLoader] = useState(false)
|
||||||
const [selectedIdWells, setSelectedIdWells] = useState([])
|
const [selectedIdWells, setSelectedIdWells] = useState([])
|
||||||
const [selectedSections, setSelectedSections] = useState([])
|
const [selectedSections, setSelectedSections] = useState([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
async () => {
|
||||||
const deposits = await DepositService.getDeposits()
|
|
||||||
const labels = {}
|
|
||||||
const wellsTree = deposits.map((deposit, dIdx) => ({
|
|
||||||
title: deposit.caption,
|
|
||||||
key: `0-${dIdx}`,
|
|
||||||
value: `0-${dIdx}`,
|
|
||||||
children: deposit.clusters.map((cluster, cIdx) => ({
|
|
||||||
title: cluster.caption,
|
|
||||||
key: `0-${dIdx}-${cIdx}`,
|
|
||||||
value: `0-${dIdx}-${cIdx}`,
|
|
||||||
children: cluster.wells.map(well => {
|
|
||||||
labels[well.id] = `${deposit.caption}.${cluster.caption}.${well.caption}`
|
|
||||||
|
|
||||||
return ({
|
|
||||||
title: well.caption,
|
|
||||||
key: well.id,
|
|
||||||
value: well.id,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
setWellsTree(wellsTree)
|
|
||||||
setWellLabels(labels)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selected = await WellCompositeService.get(idWell)
|
const selected = await WellCompositeService.get(idWell)
|
||||||
setSelectedSections(arrayOrDefault(selected))
|
setSelectedSections(arrayOrDefault(selected))
|
||||||
@ -67,8 +38,7 @@ export const WellCompositeEditor = memo(({ idWell }) => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
'Не удалось загрузить список скважин',
|
'Не удалось загрузить список скважин',
|
||||||
'Получение списка скважин'
|
'Получение списка скважин'
|
||||||
)
|
), [idWell])
|
||||||
}, [idWell])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wellIds = selectedSections.map((value) => value.idWellSrc)
|
const wellIds = selectedSections.map((value) => value.idWellSrc)
|
||||||
@ -87,24 +57,12 @@ export const WellCompositeEditor = memo(({ idWell }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<Row align={'middle'} justify={'space-between'} wrap={false}>
|
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
|
||||||
<Col span={18}>
|
<Col span={18}>
|
||||||
<TreeSelect
|
<WellSelector
|
||||||
multiple
|
idWell={idWell}
|
||||||
treeCheckable
|
onChange={setSelectedIdWells}
|
||||||
showCheckedStrategy={TreeSelect.SHOW_CHILD}
|
|
||||||
treeDefaultExpandAll
|
|
||||||
treeData={wellsTree}
|
|
||||||
treeLine={{ showLeafIcon: false }}
|
|
||||||
onChange={(value) => setSelectedIdWells(value)}
|
|
||||||
size={'middle'}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
value={selectedIdWells}
|
value={selectedIdWells}
|
||||||
placeholder={'Выберите скважины'}
|
|
||||||
tagRender={(props) => (
|
|
||||||
<Tag {...props}>{wellLabels[props.value] ?? props.label}</Tag>
|
|
||||||
)}
|
|
||||||
disabled={!hasPermission('WellOperation.edit')}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
37
src/pages/Analytics/index.jsx
Normal file
37
src/pages/Analytics/index.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Layout, Menu } from 'antd'
|
||||||
|
import { Switch, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
|
||||||
|
|
||||||
|
import WellCompositeEditor from './WellCompositeEditor'
|
||||||
|
import Statistics from './Statistics'
|
||||||
|
|
||||||
|
export const Analytics = memo(({ idWell }) => {
|
||||||
|
const { tab } = useParams()
|
||||||
|
const rootPath = `/well/${idWell}/analytics`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}>
|
||||||
|
<PrivateMenuItemLink root={rootPath} key={'composite'} path={'composite'} title={'Композитная скважина'} />
|
||||||
|
<PrivateMenuItemLink root={rootPath} key={'statistics'} path={'statistics'} title={'Оценка по ЦБ'} />
|
||||||
|
</Menu>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Content>
|
||||||
|
<Switch>
|
||||||
|
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
|
||||||
|
<WellCompositeEditor idWell={idWell} rootPath={`${rootPath}/composite`} />
|
||||||
|
</PrivateRoute>
|
||||||
|
<PrivateRoute path={`${rootPath}/statistics`}>
|
||||||
|
<Statistics idWell={idWell} />
|
||||||
|
</PrivateRoute>
|
||||||
|
<PrivateDefaultRoute urls={[`${rootPath}/composite`]}/>
|
||||||
|
</Switch>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Analytics
|
@ -1,5 +1,5 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
import { Tag, Button, Modal } from 'antd'
|
import { Tag, Button, Modal } from 'antd'
|
||||||
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
|
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
@ -16,13 +16,14 @@ import { CompanyView } from '@components/views'
|
|||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import PointerIcon from '@components/icons/PointerIcon'
|
import PointerIcon from '@components/icons/PointerIcon'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
|
||||||
import { Tvd } from '@pages/WellOperations/Tvd'
|
|
||||||
import {
|
import {
|
||||||
getOperations,
|
getOperations,
|
||||||
calcAndUpdateStatsBySections,
|
calcAndUpdateStatsBySections,
|
||||||
makeFilterMinMaxFunction
|
makeFilterMinMaxFunction
|
||||||
} from './functions'
|
} from '@utils/functions'
|
||||||
|
import { isRawDate } from '@utils'
|
||||||
|
|
||||||
|
import { Tvd } from '@pages/WellOperations/Tvd'
|
||||||
import WellOperationsTable from './WellOperationsTable'
|
import WellOperationsTable from './WellOperationsTable'
|
||||||
|
|
||||||
const filtersMinMax = [
|
const filtersMinMax = [
|
||||||
@ -34,7 +35,9 @@ const filtersWellsType = []
|
|||||||
const DAY_IN_MS = 86_400_000
|
const DAY_IN_MS = 86_400_000
|
||||||
const ONLINE_DEADTIME = 600_000
|
const ONLINE_DEADTIME = 600_000
|
||||||
|
|
||||||
export const ClusterWells = ({ statsWells }) => {
|
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
|
||||||
|
|
||||||
|
export 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)
|
||||||
@ -42,6 +45,8 @@ export const ClusterWells = ({ statsWells }) => {
|
|||||||
const [tableData, setTableData] = useState([])
|
const [tableData, setTableData] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpsModalVisible || selectedWellId <= 0) {
|
if (!isOpsModalVisible || selectedWellId <= 0) {
|
||||||
setWellOperations([])
|
setWellOperations([])
|
||||||
@ -61,14 +66,11 @@ export const ClusterWells = ({ statsWells }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let data = statsWells?.map((well) => {
|
let data = statsWells?.map((well) => {
|
||||||
if (!filtersWellsType.some((el) => el.text === well.wellType))
|
if (!filtersWellsType.some((el) => el.text === well.wellType))
|
||||||
filtersWellsType.push({ text: well.wellType, value: well.wellType,})
|
filtersWellsType.push({ text: well.wellType, value: well.wellType })
|
||||||
|
|
||||||
let periodPlanValue = well.total?.plan?.start && well.total?.plan?.end
|
const dateOrM = (a, b) => a && b ? (new Date(b) - new Date(a)) / DAY_IN_MS : '-'
|
||||||
? (new Date(well.total?.plan?.end) - new Date(well.total?.plan?.start)) / DAY_IN_MS
|
const periodPlanValue = dateOrM(well.total?.plan?.start, well.total?.plan?.end)
|
||||||
: '-'
|
const periodFactValue = dateOrM(well.total?.fact?.start, well.total?.fact?.end)
|
||||||
let periodFactValue = well.total?.fact?.start && well.total?.fact?.end
|
|
||||||
? (new Date(well.total?.fact?.end) - new Date(well.total?.fact?.start)) / DAY_IN_MS
|
|
||||||
: '-'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: well.caption,
|
key: well.caption,
|
||||||
@ -106,14 +108,10 @@ export const ClusterWells = ({ statsWells }) => {
|
|||||||
setTableData(data)
|
setTableData(data)
|
||||||
}, [statsWells])
|
}, [statsWells])
|
||||||
|
|
||||||
const getDate = (str) => Number.isNaN(+new Date(str)) || +new Date(str) === 0
|
|
||||||
? '-'
|
|
||||||
: new Date(str).toLocaleString()
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
makeTextColumn('скв №', 'caption', null, null,
|
makeTextColumn('скв №', 'caption', null, null,
|
||||||
(_, item) => (
|
(_, item) => (
|
||||||
<Link to={`/well/${item.id}`} style={{display: 'flex', alignItems: 'center'}}>
|
<Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}>
|
||||||
<PointerIcon
|
<PointerIcon
|
||||||
state={item.idState === 1 ? 'active' : 'unknown'}
|
state={item.idState === 1 ? 'active' : 'unknown'}
|
||||||
width={32}
|
width={32}
|
||||||
@ -172,6 +170,7 @@ export const ClusterWells = ({ statsWells }) => {
|
|||||||
bordered
|
bordered
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey={(record) => record.caption}
|
rowKey={(record) => record.caption}
|
||||||
|
tableName={'cluster'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@ -199,6 +198,6 @@ export const ClusterWells = ({ statsWells }) => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default ClusterWells
|
export default ClusterWells
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
import { Table } from 'antd'
|
import { Table } from 'antd'
|
||||||
|
|
||||||
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
|
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
|
||||||
|
import { getPrecision } from '@utils/functions'
|
||||||
import { getPrecision } from './functions'
|
|
||||||
|
|
||||||
export const WellOperationsTable = ({ wellOperations }) => {
|
export const WellOperationsTable = ({ wellOperations }) => {
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -35,6 +33,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
|
|||||||
dataSource={operations}
|
dataSource={operations}
|
||||||
rowKey={(record) => record.key}
|
rowKey={(record) => record.key}
|
||||||
pagination={{ defaultPageSize: 10 }}
|
pagination={{ defaultPageSize: 10 }}
|
||||||
|
tableName={'well_operations'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
import { OperationStatService } from '@api'
|
|
||||||
|
|
||||||
const maxPrefix = 'isMax'
|
|
||||||
const minPrefix = 'isMin'
|
|
||||||
|
|
||||||
export const getPrecision = (number) => Number.isFinite(number) ? number.toFixed(2) : '-'
|
|
||||||
|
|
||||||
export const getOperations = async (idWell) => {
|
|
||||||
const ops = await OperationStatService.getTvd(idWell)
|
|
||||||
|
|
||||||
if (!ops) return []
|
|
||||||
|
|
||||||
const convert = wellOperationDto => ({
|
|
||||||
key: wellOperationDto?.id,
|
|
||||||
depth: wellOperationDto?.depthStart,
|
|
||||||
date: wellOperationDto?.dateStart,
|
|
||||||
day: wellOperationDto?.day,
|
|
||||||
})
|
|
||||||
|
|
||||||
const planData = ops
|
|
||||||
.map(item => convert(item.plan))
|
|
||||||
.filter(el => el.key)
|
|
||||||
|
|
||||||
const factData = ops
|
|
||||||
.map(item => convert(item.fact))
|
|
||||||
.filter(el => el.key)
|
|
||||||
|
|
||||||
const predictData = ops
|
|
||||||
.map(item => convert(item.predict))
|
|
||||||
.filter(el => el.key)
|
|
||||||
|
|
||||||
return { operations: ops, plan: planData, fact: factData, predict: predictData }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeFilterMinMaxFunction = (key) => (filterValue,
|
|
||||||
dataItem) =>
|
|
||||||
filterValue === 'max'
|
|
||||||
? dataItem[maxPrefix + key]
|
|
||||||
: filterValue === 'min'
|
|
||||||
? dataItem[minPrefix + key]
|
|
||||||
: false
|
|
||||||
|
|
||||||
export const calcAndUpdateStats = (data, keys) => {
|
|
||||||
const mins = {}
|
|
||||||
const maxs = {}
|
|
||||||
|
|
||||||
keys.forEach((key) => {
|
|
||||||
maxs[key] = Number.MIN_VALUE
|
|
||||||
mins[key] = Number.MAX_VALUE
|
|
||||||
})
|
|
||||||
|
|
||||||
data.forEach((item) => {
|
|
||||||
keys.forEach((key) => {
|
|
||||||
if (mins[key] > item[key]) mins[key] = item[key]
|
|
||||||
if (maxs[key] < item[key]) maxs[key] = item[key]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
keys.forEach((key) => {
|
|
||||||
data[i][maxPrefix + key] = data[i][key] === maxs[key]
|
|
||||||
data[i][minPrefix + key] = data[i][key] === mins[key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const calcAndUpdateStatsBySections = (data, keys) => {
|
|
||||||
const sectionTypes = new Set()
|
|
||||||
data.forEach((item) => sectionTypes.add(item.sectionType))
|
|
||||||
sectionTypes.forEach(sectionType => {
|
|
||||||
const filteredBySectionData = data.filter((item) => item.sectionType === sectionType)
|
|
||||||
calcAndUpdateStats(filteredBySectionData, keys)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Map, Overlay } from 'pigeon-maps'
|
import { Map, Overlay } from 'pigeon-maps'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
|
|
||||||
import { ClusterService } from '@api'
|
import { ClusterService } from '@api'
|
||||||
@ -8,6 +8,8 @@ import { PointerIcon } from '@components/icons'
|
|||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
|
||||||
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
|
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
|
||||||
|
|
||||||
const calcViewParams = (clusters) => {
|
const calcViewParams = (clusters) => {
|
||||||
@ -41,6 +43,8 @@ export const Deposit = memo(() => {
|
|||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [viewParams, setViewParams] = useState(defaultViewParams)
|
const [viewParams, setViewParams] = useState(defaultViewParams)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const data = await ClusterService.getClusters()
|
const data = await ClusterService.getClusters()
|
||||||
@ -62,7 +66,7 @@ export const Deposit = memo(() => {
|
|||||||
anchor={[cluster.latitude, cluster.longitude]}
|
anchor={[cluster.latitude, cluster.longitude]}
|
||||||
key={`${cluster.latitude} ${cluster.longitude}`}
|
key={`${cluster.latitude} ${cluster.longitude}`}
|
||||||
>
|
>
|
||||||
<Link to={`/cluster/${cluster.id}`}>
|
<Link to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
|
||||||
<PointerIcon state={'active'} width={48} height={59} />
|
<PointerIcon state={'active'} width={48} height={59} />
|
||||||
<span>{cluster.caption}</span>
|
<span>{cluster.caption}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import moment from 'moment'
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { DatePicker, Button, Input } from 'antd'
|
import { DatePicker, Button, Input } from 'antd'
|
||||||
|
|
||||||
@ -9,6 +8,7 @@ import { UploadForm } from '@components/UploadForm'
|
|||||||
import { CompanyView, UserView } from '@components/views'
|
import { CompanyView, UserView } from '@components/views'
|
||||||
import { EditableTable, makePaginationObject } from '@components/Table'
|
import { EditableTable, makePaginationObject } from '@components/Table'
|
||||||
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
|
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
|
||||||
|
import { formatDate } from '@utils'
|
||||||
|
|
||||||
const pageSize = 12
|
const pageSize = 12
|
||||||
const { RangePicker } = DatePicker
|
const { RangePicker } = DatePicker
|
||||||
@ -28,7 +28,7 @@ const columns = [
|
|||||||
title: 'Дата загрузки',
|
title: 'Дата загрузки',
|
||||||
key: 'uploadDate',
|
key: 'uploadDate',
|
||||||
dataIndex: 'uploadDate',
|
dataIndex: 'uploadDate',
|
||||||
render: item => moment.utc(item).local().format('DD MMM YYYY, HH:mm:ss'),
|
render: item => formatDate(item, false, 'DD MMM YYYY, HH:mm:ss'),
|
||||||
}, {
|
}, {
|
||||||
title: 'Размер',
|
title: 'Размер',
|
||||||
key: 'size',
|
key: 'size',
|
||||||
@ -46,7 +46,7 @@ const columns = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange}) => {
|
export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange, tableName }) => {
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [filterDataRange, setFilterDataRange] = useState([])
|
const [filterDataRange, setFilterDataRange] = useState([])
|
||||||
const [filterCompanyName, setFilterCompanyName] = useState([])
|
const [filterCompanyName, setFilterCompanyName] = useState([])
|
||||||
@ -173,6 +173,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
|
|||||||
}}
|
}}
|
||||||
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete}
|
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete}
|
||||||
rowKey={(record) => record.id}
|
rowKey={(record) => record.id}
|
||||||
|
tableName={tableName ?? `file_${idCategory}`}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -46,7 +46,7 @@ export const MenuDocuments = memo(({ idWell }) => {
|
|||||||
<Switch>
|
<Switch>
|
||||||
{documentCategories.map(category => (
|
{documentCategories.map(category => (
|
||||||
<PrivateRoute path={join(root, category.key)} key={`${category.key}`}>
|
<PrivateRoute path={join(root, category.key)} key={`${category.key}`}>
|
||||||
<DocumentsTemplate idCategory={category.id} idWell={idWell} />
|
<DocumentsTemplate idCategory={category.id} idWell={idWell} tableName={`documents_${category.key}`} />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
))}
|
))}
|
||||||
<PrivateDefaultRoute urls={documentCategories.map((cat) => join(root, cat.key))}/>
|
<PrivateDefaultRoute urls={documentCategories.map((cat) => join(root, cat.key))}/>
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { FileExcelOutlined } from '@ant-design/icons'
|
|
||||||
import { Popconfirm, Button, Tooltip, Typography } from 'antd'
|
|
||||||
|
|
||||||
import { Flex } from '@components/Grid'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { MarkView, UserView } from '@components/views'
|
|
||||||
import { invokeWebApiWrapperAsync, download, formatBytes } from '@components/factory'
|
|
||||||
import { DrillingProgramService, WellService } from '@api'
|
|
||||||
|
|
||||||
import DocumentsTemplate from '@pages/Documents/DocumentsTemplate'
|
|
||||||
|
|
||||||
const idFileCategoryDrillingProgramItems = 13
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
export const DrillingProgram = ({ idWell }) => {
|
|
||||||
const [downloadButtonEnabled, selDownloadButtonEnabled] = useState(false)
|
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
|
||||||
const [tooltip, setTooltip] = useState('нет файлов для формирования')
|
|
||||||
const [wellLabel, setWellLabel] = useState(`${idWell}`)
|
|
||||||
const [childKey, setChildKey] = useState()
|
|
||||||
const [lastUpdatedFile, setLastUpdatedFile] = useState()
|
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
const well = await WellService.get(idWell)
|
|
||||||
setWellLabel(well.caption ?? `${idWell}`)
|
|
||||||
},
|
|
||||||
setShowLoader,
|
|
||||||
`Не удалось загрузить название скважины "${idWell}"`,
|
|
||||||
'Получить название скважины'
|
|
||||||
), [idWell])
|
|
||||||
|
|
||||||
const downloadProgram = () => invokeWebApiWrapperAsync(
|
|
||||||
async () => await download(`/api/well/${idWell}/drillingProgram`),
|
|
||||||
setShowLoader,
|
|
||||||
`Не удалось загрузить программу бурения для скважины "${idWell}"`,
|
|
||||||
'Получить программу бурения'
|
|
||||||
)
|
|
||||||
|
|
||||||
const openProgramPreview = () => invokeWebApiWrapperAsync(
|
|
||||||
async() => {
|
|
||||||
const filWebUrl = await DrillingProgramService.getOrCreateSharedUrl(idWell)
|
|
||||||
if(filWebUrl && filWebUrl.length > 0)
|
|
||||||
window.open(filWebUrl, '_blank')
|
|
||||||
else
|
|
||||||
throw new Error('Сервер вернул плохую ссылку')
|
|
||||||
},
|
|
||||||
setShowLoader,
|
|
||||||
'Не удалось создать быстрый просмотр программы',
|
|
||||||
'Создать быстрый просмотр программы'
|
|
||||||
)
|
|
||||||
|
|
||||||
const filesUpdated = (files) => {
|
|
||||||
if (!files || files.length === 0) {
|
|
||||||
setTooltip('Нет файлов для формирования программы')
|
|
||||||
selDownloadButtonEnabled(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAllFilesAreExcel = files.every(fileInfo => fileInfo?.name.toLowerCase().endsWith('.xlsx'))
|
|
||||||
const isAnyFileMarked = files.some(file => file?.fileMarks.some(m => m?.idMarkType === 1 && !m?.isDeleted))
|
|
||||||
|
|
||||||
if (isAllFilesAreExcel && isAnyFileMarked) {
|
|
||||||
setTooltip('Программа доступна для скачивания')
|
|
||||||
selDownloadButtonEnabled(true)
|
|
||||||
} else {
|
|
||||||
setTooltip('Список файлов содержит недопустимые типы файлов')
|
|
||||||
}
|
|
||||||
const last = files.reduce((pre, cur) => pre.uploadDate > cur.uploadDate ? pre : cur)
|
|
||||||
setLastUpdatedFile(last)
|
|
||||||
}
|
|
||||||
|
|
||||||
const customColumns = [
|
|
||||||
{
|
|
||||||
title: 'Метки',
|
|
||||||
key: 'fileMarks',
|
|
||||||
render: (_, record) => renderMarksColumn(record?.id, record?.fileMarks)
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const renderMarksColumn=(idFile, marks)=>{
|
|
||||||
const validMarks = marks?.filter(m => !(m?.isDeleted))
|
|
||||||
if(validMarks?.length)
|
|
||||||
return validMarks.map(mark => <MarkView mark = {mark} onDelete={() => deleteMark(mark.id)}/>)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popconfirm title={'Согласовать файл?'} onConfirm={() => addMarkToFile(idFile)}>
|
|
||||||
<Button type={'link'}>Согласовать</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addMarkToFile = async (idFile) => {
|
|
||||||
const mark = {
|
|
||||||
idFile: idFile,
|
|
||||||
idMarkType: 1,
|
|
||||||
isDeleted: false,
|
|
||||||
comment: '',
|
|
||||||
}
|
|
||||||
await DrillingProgramService.createFileMark(idWell, mark)
|
|
||||||
selDownloadButtonEnabled(true)
|
|
||||||
setChildKey(Date.now())
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMark = async (idMark) => {
|
|
||||||
await DrillingProgramService.deleteFileMark(idWell, idMark)
|
|
||||||
setChildKey(Date.now())
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadButton = (
|
|
||||||
<div>
|
|
||||||
<span>Программа бурения</span>
|
|
||||||
<Flex>
|
|
||||||
<Tooltip title={tooltip}>
|
|
||||||
<Button
|
|
||||||
type={'primary'}
|
|
||||||
onClick={downloadProgram}
|
|
||||||
disabled={!downloadButtonEnabled}
|
|
||||||
>
|
|
||||||
Сформировать и скачать
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={'Просмотреть через GoogleDrive'}>
|
|
||||||
<Popconfirm
|
|
||||||
title={'Загрузить файл на GoogleDrive для просмотра?'}
|
|
||||||
onConfirm={openProgramPreview}
|
|
||||||
disabled={!downloadButtonEnabled}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type={'link'}
|
|
||||||
disabled={!downloadButtonEnabled}
|
|
||||||
>
|
|
||||||
<FileExcelOutlined />
|
|
||||||
Программа бурения {wellLabel}.xlsx
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const lastUpdatedFileView = lastUpdatedFile &&
|
|
||||||
<Text>
|
|
||||||
<b>Последнее изменние:</b>
|
|
||||||
'{lastUpdatedFile.name}'
|
|
||||||
[{formatBytes(lastUpdatedFile.size)}]
|
|
||||||
загружен: {new Date(lastUpdatedFile.uploadDate).toLocaleString()}
|
|
||||||
автор: <UserView user={lastUpdatedFile.author}/>
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoaderPortal show={showLoader}>
|
|
||||||
<DocumentsTemplate
|
|
||||||
key={childKey}
|
|
||||||
idWell={idWell}
|
|
||||||
accept={'.xlsx'}
|
|
||||||
onChange={filesUpdated}
|
|
||||||
headerChild={downloadButton}
|
|
||||||
customColumns={customColumns}
|
|
||||||
beforeTable={lastUpdatedFileView}
|
|
||||||
idCategory={idFileCategoryDrillingProgramItems}
|
|
||||||
/>
|
|
||||||
</LoaderPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DrillingProgram
|
|
77
src/pages/DrillingProgram/CategoryAdder.jsx
Normal file
77
src/pages/DrillingProgram/CategoryAdder.jsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Form, Select } from 'antd'
|
||||||
|
import { FileAddOutlined } from '@ant-design/icons'
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import Poprompt from '@components/Poprompt'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { DrillingProgramService } from '@api'
|
||||||
|
|
||||||
|
import '@styles/drilling_program.less'
|
||||||
|
|
||||||
|
const catSelectorRules = [{
|
||||||
|
required: true,
|
||||||
|
message: 'Пожалуйста, выберите категории'
|
||||||
|
}]
|
||||||
|
|
||||||
|
export const CategoryAdder = memo(({ categories, idWell, onUpdate, className, ...other }) => {
|
||||||
|
const [options, setOptions] = useState([])
|
||||||
|
const [value, setValue] = useState([])
|
||||||
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
const [showCatLoader, setShowCatLoader] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
setOptions(categories.map((category) => ({
|
||||||
|
label: category.name ?? category.shortName,
|
||||||
|
value: category.id
|
||||||
|
})))
|
||||||
|
},
|
||||||
|
setShowCatLoader,
|
||||||
|
`Не удалось установить список доступных категорий для добавления`
|
||||||
|
), [categories])
|
||||||
|
|
||||||
|
const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
if (!categories) return
|
||||||
|
await DrillingProgramService.addParts(idWell, categories)
|
||||||
|
setValue([])
|
||||||
|
onUpdate?.()
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось добавить новые категорий программы бурения`,
|
||||||
|
`Добавление категорий программы бурения`
|
||||||
|
), [onUpdate, idWell])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Poprompt
|
||||||
|
buttonProps={{
|
||||||
|
icon: <FileAddOutlined />,
|
||||||
|
loading: showLoader,
|
||||||
|
}}
|
||||||
|
placement={'topLeft'}
|
||||||
|
text={'Добавить категорию'}
|
||||||
|
onDone={onFinish}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name={'categories'}
|
||||||
|
label={'Категории:'}
|
||||||
|
className={`category_adder ${className ?? ''}`}
|
||||||
|
rules={catSelectorRules}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{ width: '300px' }}
|
||||||
|
allowClear
|
||||||
|
className={'adder_select'}
|
||||||
|
mode={'multiple'}
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
loading={showCatLoader}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Poprompt>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CategoryAdder
|
171
src/pages/DrillingProgram/CategoryEditor.jsx
Normal file
171
src/pages/DrillingProgram/CategoryEditor.jsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { Input, Modal, Radio } from 'antd'
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
|
||||||
|
|
||||||
|
import { UserView } from '@components/views'
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { makeColumn, makeNumericSorter, Table } from '@components/Table'
|
||||||
|
import { DrillingProgramService } from '@api'
|
||||||
|
import { arrayOrDefault } from '@utils'
|
||||||
|
|
||||||
|
const userRules = [
|
||||||
|
{ label: 'Нет', value: 0 },
|
||||||
|
{ label: 'Публикатор', value: 1 },
|
||||||
|
{ label: 'Согласующий', value: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const SEARCH_TIMEOUT = 400
|
||||||
|
|
||||||
|
export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) => {
|
||||||
|
const [title, setTitle] = useState()
|
||||||
|
const [users, setUsers] = useState([])
|
||||||
|
const [allUsers, setAllUsers] = useState([])
|
||||||
|
const [filteredUsers, setFilteredUsers] = useState([])
|
||||||
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
const [subject, setSubject] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const filteredUsers = users.filter(({ user }) => user && [
|
||||||
|
user.login ?? '',
|
||||||
|
user.name ?? '',
|
||||||
|
user.surname ?? '',
|
||||||
|
user.partonymic ?? '',
|
||||||
|
user.email ?? '',
|
||||||
|
user.phone ?? '',
|
||||||
|
user.position ?? '',
|
||||||
|
user.company?.caption ?? '',
|
||||||
|
].join(' ').toLowerCase().includes(searchValue))
|
||||||
|
setFilteredUsers(filteredUsers)
|
||||||
|
},
|
||||||
|
setIsSearching,
|
||||||
|
`Не удалось произвести поиск пользователей`
|
||||||
|
), [users, searchValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subject) {
|
||||||
|
const sub = new BehaviorSubject('')
|
||||||
|
setSubject(sub)
|
||||||
|
} else {
|
||||||
|
const observable = subject.pipe(
|
||||||
|
debounceTime(SEARCH_TIMEOUT),
|
||||||
|
map(s => s.trim().toLowerCase() ?? ''),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter(s => s.length <= 0 || s.length >= 2),
|
||||||
|
).subscribe(value => setSearchValue(value))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observable.unsubscribe()
|
||||||
|
subject.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [subject])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell))
|
||||||
|
setAllUsers(allUsers)
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось загрузить список доступных пользователей скважины "${idWell}"`
|
||||||
|
), [idWell])
|
||||||
|
|
||||||
|
const calcUsers = useCallback(() => {
|
||||||
|
if (!visible) return
|
||||||
|
setTitle(category?.name ? `"${category.name}"` : '')
|
||||||
|
|
||||||
|
const approvers = arrayOrDefault(category?.approvers)
|
||||||
|
const publishers = arrayOrDefault(category?.publishers)
|
||||||
|
const otherUsers = allUsers.filter((user) => [...approvers, ...publishers].findIndex(u => u.id === user.id) < 0)
|
||||||
|
const users = [
|
||||||
|
...otherUsers.map((user) => ({ user, status: 0 })),
|
||||||
|
...publishers.map((user) => ({ user, status: 1 })),
|
||||||
|
...approvers.map((user) => ({ user, status: 2 })),
|
||||||
|
]
|
||||||
|
setUsers(users)
|
||||||
|
}, [category, visible, allUsers])
|
||||||
|
|
||||||
|
useEffect(calcUsers, [calcUsers])
|
||||||
|
|
||||||
|
const changeUserStatus = useCallback((user, status) => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const userIdx = users?.findIndex(({ user: u }) => u.id === user.id)
|
||||||
|
if (!userIdx) return
|
||||||
|
|
||||||
|
if (status === 0) {
|
||||||
|
await DrillingProgramService.removeUser(idWell, user.id, category.idFileCategory, users[userIdx].status)
|
||||||
|
} else {
|
||||||
|
await DrillingProgramService.addUser(idWell, user.id, category.idFileCategory, status)
|
||||||
|
}
|
||||||
|
setUsers((prevUsers) => {
|
||||||
|
prevUsers[userIdx].status = status
|
||||||
|
return prevUsers
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
<>
|
||||||
|
Не удалось изменить статус пользователя
|
||||||
|
<UserView user={user} />
|
||||||
|
</>,
|
||||||
|
`Изменение статуса пользователя`
|
||||||
|
), [users, idWell, category.idFileCategory])
|
||||||
|
|
||||||
|
const userColumns = [
|
||||||
|
makeColumn('Пользователь', 'user', {
|
||||||
|
sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0,
|
||||||
|
render: (user) => <UserView user={user} />,
|
||||||
|
}),
|
||||||
|
makeColumn('Статус', 'status', {
|
||||||
|
sorter: makeNumericSorter('status'),
|
||||||
|
defaultSortOrder: 'descend',
|
||||||
|
render: (status, { user }) => (
|
||||||
|
<Radio.Group
|
||||||
|
options={userRules}
|
||||||
|
value={status ?? 0}
|
||||||
|
onChange={(e) => changeUserStatus(user, e?.target?.value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
|
||||||
|
|
||||||
|
const onModalClosed = useCallback(() => {
|
||||||
|
onClosed?.()
|
||||||
|
calcUsers()
|
||||||
|
}, [onClosed, calcUsers])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
centered
|
||||||
|
width={1000}
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={onModalClosed}
|
||||||
|
title={`Редактирование пользователей категории ${title}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
placeholder={'Поиск пользователя'}
|
||||||
|
onChange={onSearchTextChange}
|
||||||
|
style={{ marginBottom: '15px' }}
|
||||||
|
loading={isSearching}
|
||||||
|
/>
|
||||||
|
<LoaderPortal show={showLoader}>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
columns={userColumns}
|
||||||
|
dataSource={filteredUsers}
|
||||||
|
tableName={`drilling_program_category_editor`}
|
||||||
|
/>
|
||||||
|
</LoaderPortal>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CategoryEditor
|
128
src/pages/DrillingProgram/CategoryHistory.jsx
Normal file
128
src/pages/DrillingProgram/CategoryHistory.jsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { Button, DatePicker, Input, Modal } from 'antd'
|
||||||
|
|
||||||
|
import { CompanyView } from '@components/views'
|
||||||
|
import DownloadLink from '@components/DownloadLink'
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
|
||||||
|
import { arrayOrDefault, formatDate } from '@utils'
|
||||||
|
import { FileService } from '@api'
|
||||||
|
|
||||||
|
import MarksCard from './MarksCard'
|
||||||
|
|
||||||
|
import '@styles/drilling_program.less'
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker
|
||||||
|
const { Search } = Input
|
||||||
|
|
||||||
|
export const historyColumns = [
|
||||||
|
makeColumn('Имя файла', 'name', {
|
||||||
|
sorter: makeStringSorter('name'),
|
||||||
|
render: (name, file) => <DownloadLink file={file} name={name} />
|
||||||
|
}),
|
||||||
|
makeColumn('Компания', 'author', {
|
||||||
|
sorter: (a, b) => makeStringSorter('caption')(a?.author?.company, b?.author?.company),
|
||||||
|
render: (author) => <CompanyView company={author?.company}/>
|
||||||
|
}),
|
||||||
|
makeColumn('Дата публикации', 'uploadDate', {
|
||||||
|
sorter: makeDateSorter('uploadDate'),
|
||||||
|
render: (date) => formatDate(date),
|
||||||
|
}),
|
||||||
|
makeColumn('Отметки', 'fileMarks', {
|
||||||
|
render: (marks) => {
|
||||||
|
const approved = [], rejected = []
|
||||||
|
arrayOrDefault(marks).forEach((mark) => {
|
||||||
|
if (mark.idMarkType === 0) rejected.push(mark)
|
||||||
|
if (mark.idMarkType === 1) approved.push(mark)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'history_approve_column'} >
|
||||||
|
<div className={'approve_list'}>
|
||||||
|
<MarksCard title={'Согласовано'} className={'approve_panel'} marks={approved} />
|
||||||
|
</div>
|
||||||
|
<div className={'reject_list'}>
|
||||||
|
<MarksCard title={'Отклонено'} className={'reject_panel'} marks={rejected} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
export const CategoryHistory = ({ idWell, idCategory, visible, onClose }) => {
|
||||||
|
const [data, setData] = useState([])
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [pageSize, setPageSize] = useState(14)
|
||||||
|
const [range, setRange] = useState([])
|
||||||
|
const [fileName, setFileName] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [companyName, setCompanyName] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
if (!visible) return
|
||||||
|
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)
|
||||||
|
setTotal(paginatedHistory?.count ?? 0)
|
||||||
|
setData(arrayOrDefault(paginatedHistory?.items))
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
`Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"`
|
||||||
|
), [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
|
||||||
|
|
||||||
|
const onPaginationChange = useCallback((page, pageSize) => {
|
||||||
|
setPage(page)
|
||||||
|
setPageSize(pageSize)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={'История категории'}
|
||||||
|
width={1200}
|
||||||
|
centered
|
||||||
|
visible={!!visible}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={(
|
||||||
|
<Button onClick={onClose}>Закрыть</Button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LoaderPortal show={isLoading}>
|
||||||
|
<div className={'filter-group'}>
|
||||||
|
<div className={'filter-group-heading'}>
|
||||||
|
<Search
|
||||||
|
className={'filter-selector'}
|
||||||
|
placeholder={'Фильтр по имени файла'}
|
||||||
|
onSearch={(value) => setFileName(value)}
|
||||||
|
/>
|
||||||
|
<Search
|
||||||
|
className={'filter-selector'}
|
||||||
|
placeholder={'Фильтр по названии компании'}
|
||||||
|
onSearch={(value) => setCompanyName(value)}
|
||||||
|
/>
|
||||||
|
<RangePicker showTime onChange={(range) => setRange(range)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
size={'small'}
|
||||||
|
columns={historyColumns}
|
||||||
|
dataSource={data}
|
||||||
|
pagination={{
|
||||||
|
total: total,
|
||||||
|
current: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
showSizeChanger: true,
|
||||||
|
onChange: onPaginationChange
|
||||||
|
}}
|
||||||
|
tableName={'drilling_program_history'}
|
||||||
|
/>
|
||||||
|
</LoaderPortal>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryHistory
|
169
src/pages/DrillingProgram/CategoryRender.jsx
Normal file
169
src/pages/DrillingProgram/CategoryRender.jsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { memo, useCallback, useState } from 'react'
|
||||||
|
import { Button, Input, Popconfirm, Form } from 'antd'
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
TableOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import Poprompt from '@components/Poprompt'
|
||||||
|
import { UserView } from '@components/views'
|
||||||
|
import UploadForm from '@components/UploadForm'
|
||||||
|
import DownloadLink from '@components/DownloadLink'
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory'
|
||||||
|
import { DrillingProgramService } from '@api'
|
||||||
|
import { formatDate } from '@utils'
|
||||||
|
|
||||||
|
import MarksCard from './MarksCard'
|
||||||
|
|
||||||
|
import '@styles/drilling_program.less'
|
||||||
|
|
||||||
|
const CommentPrompt = memo(({ isRequired = true, ...props }) => (
|
||||||
|
<Poprompt
|
||||||
|
buttonProps={{ className: 'mv-5' }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={'Комментарий'}
|
||||||
|
name={'comment'}
|
||||||
|
rules={isRequired && [{ required: true, message: 'Пожалуйста, введите комментарий!' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Poprompt>
|
||||||
|
))
|
||||||
|
|
||||||
|
export const CategoryRender = memo(({ idWell, partData, onUpdate, onEdit, onHistory, setIsLoading, ...other }) => {
|
||||||
|
const {
|
||||||
|
idFileCategory,
|
||||||
|
name: title, // Название категории
|
||||||
|
approvers, // Полный список согласовантов
|
||||||
|
permissionToApprove, // Наличие прав на согласование/отклонение документа
|
||||||
|
permissionToUpload, // Наличие прав на загрузку нового файла
|
||||||
|
file // Информация о файле
|
||||||
|
} = partData ?? {}
|
||||||
|
|
||||||
|
const uploadUrl = `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`
|
||||||
|
|
||||||
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
|
||||||
|
const onApprove = useCallback((approve = true) => (values) => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
if (!file?.id || !permissionToApprove) return
|
||||||
|
await DrillingProgramService.addOrReplaceFileMark(idWell, {
|
||||||
|
idFile: file.id,
|
||||||
|
idMarkType: approve ? 1 : 0,
|
||||||
|
comment: values.comment
|
||||||
|
})
|
||||||
|
await onUpdate?.()
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
`Не удалось ${approve ? 'согласовать' : 'отклонить'} документ для скважины "${idWell}"!`,
|
||||||
|
`${approve ? 'Согласование' : 'Отклонение'} документа "${title}" скважины "${idWell}"`
|
||||||
|
), [idWell, setIsLoading, file, permissionToApprove, title, onUpdate])
|
||||||
|
|
||||||
|
const onRemoveClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
await DrillingProgramService.removeParts(idWell, [idFileCategory])
|
||||||
|
onUpdate?.()
|
||||||
|
},
|
||||||
|
setIsDeleting,
|
||||||
|
`Не удалось удалить категорию "${title}" для скважины "${idWell}"`,
|
||||||
|
`Удаление категории "${title}" скважины "${idWell}"`
|
||||||
|
), [idWell, idFileCategory, onUpdate, title])
|
||||||
|
|
||||||
|
const onUploadComplete = useCallback(() => {
|
||||||
|
onUpdate?.(idFileCategory)
|
||||||
|
setIsUploading(false)
|
||||||
|
}, [onUpdate, idFileCategory])
|
||||||
|
|
||||||
|
const onUploadError = useCallback((e) => {
|
||||||
|
notify(e?.message ?? 'Ошибка загрузки файла', 'error')
|
||||||
|
setIsUploading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={isUploading}>
|
||||||
|
<div className={'drilling_category'} {...other}>
|
||||||
|
<div className={'category_header'}>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{onEdit && (
|
||||||
|
<div>
|
||||||
|
<Button icon={<EditOutlined />} onClick={() => onEdit?.(idFileCategory)}>Редактировать</Button>
|
||||||
|
<Popconfirm
|
||||||
|
onConfirm={onRemoveClick}
|
||||||
|
title={<>Вы уверены, что хотите удалить категорию<br/>"{title}"?</>}
|
||||||
|
>
|
||||||
|
<Button loading={isDeleting} icon={<DeleteOutlined />}>Удалить</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'category_content'}>
|
||||||
|
<div className={'file_column'}>
|
||||||
|
<div className={'file_info'}>
|
||||||
|
{file ? (
|
||||||
|
<>
|
||||||
|
<DownloadLink file={file} />
|
||||||
|
<div className={'ml-15'}>Автор: <UserView user={file.author ?? '-'}/></div>
|
||||||
|
<div className={'ml-15'}>Размер: {file.size ? formatBytes(file.size) : '-'}</div>
|
||||||
|
<div className={'ml-15'}>Загружен: {formatDate(file.uploadDate) ?? '-'}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>Нет загруженных файлов</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'file_actions'}>
|
||||||
|
{permissionToUpload && (
|
||||||
|
<UploadForm
|
||||||
|
url={uploadUrl}
|
||||||
|
style={{ margin: '5px 0 10px 0' }}
|
||||||
|
onUploadStart={() => setIsUploading(true)}
|
||||||
|
onUploadComplete={onUploadComplete}
|
||||||
|
onUploadError={onUploadError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
disabled={!file}
|
||||||
|
title={'История'}
|
||||||
|
icon={<TableOutlined />}
|
||||||
|
onClick={() => onHistory?.(idFileCategory)}
|
||||||
|
>
|
||||||
|
История
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'approve_column'}>
|
||||||
|
<div className={'user_list'}>
|
||||||
|
{approvers.map((user, i) => (
|
||||||
|
<span key={i}>
|
||||||
|
<UserView user={user} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{file && (
|
||||||
|
<>
|
||||||
|
<div className={'approve_list'}>
|
||||||
|
{permissionToApprove && (
|
||||||
|
<CommentPrompt isRequired={false} text={'Согласовать'} title={'Согласование документа'} onDone={onApprove(true)} />
|
||||||
|
)}
|
||||||
|
<MarksCard title={'Согласовано'} className={'approve_panel'} marks={file?.fileMarks?.filter((mark) => mark.idMarkType === 1)} />
|
||||||
|
</div>
|
||||||
|
<div className={'reject_list'}>
|
||||||
|
{permissionToApprove && (
|
||||||
|
<CommentPrompt text={'Отклонить'} title={'Отклонение документа'} onDone={onApprove(false)} />
|
||||||
|
)}
|
||||||
|
<MarksCard title={'Отклонено'} className={'reject_panel'} marks={file?.fileMarks?.filter((mark) => mark.idMarkType === 0)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CategoryRender
|
25
src/pages/DrillingProgram/MarksCard.jsx
Normal file
25
src/pages/DrillingProgram/MarksCard.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { CommentOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { UserView } from '@components/views'
|
||||||
|
import { formatDate } from '@utils'
|
||||||
|
|
||||||
|
export const MarksCard = memo(({ title, marks, className, ...other }) => (
|
||||||
|
<div className={`panel ${className ?? ''}`} {...other}>
|
||||||
|
<span className={'panel_title'}>{title}</span>
|
||||||
|
<div className={'panel_content'}>
|
||||||
|
{marks?.map(({ dateCreated, comment, user }, i) => (
|
||||||
|
<div key={`${i}`}>
|
||||||
|
<UserView user={user} />
|
||||||
|
<span>{formatDate(dateCreated)}</span>
|
||||||
|
<Tooltip title={comment}>
|
||||||
|
<CommentOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default MarksCard
|
176
src/pages/DrillingProgram/index.jsx
Normal file
176
src/pages/DrillingProgram/index.jsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { Button, Layout } from 'antd'
|
||||||
|
import {
|
||||||
|
AuditOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
FileWordOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { arrayOrDefault, formatDate } from '@utils'
|
||||||
|
import { DrillingProgramService } from '@api'
|
||||||
|
|
||||||
|
import CategoryAdder from './CategoryAdder'
|
||||||
|
import CategoryRender from './CategoryRender'
|
||||||
|
import CategoryEditor from './CategoryEditor'
|
||||||
|
import CategoryHistory from './CategoryHistory'
|
||||||
|
|
||||||
|
import '@styles/drilling_program.less'
|
||||||
|
|
||||||
|
const idStateNotInitialized = 0
|
||||||
|
const idStateApproving = 1
|
||||||
|
const idStateCreating = 2
|
||||||
|
const idStateReady = 3
|
||||||
|
const idStateError = 4
|
||||||
|
const idStateUnknown = -1
|
||||||
|
|
||||||
|
const stateString = {
|
||||||
|
[idStateNotInitialized]: { icon: CloseOutlined, text: 'Не настроена' },
|
||||||
|
[idStateApproving]: { icon: AuditOutlined, text: 'Согласовывается' },
|
||||||
|
[idStateCreating]: { icon: LoadingOutlined, text: 'Формируется' },
|
||||||
|
[idStateReady]: { icon: CheckOutlined, text: 'Сформирована' },
|
||||||
|
[idStateError]: { icon: WarningOutlined, text: 'Ошибка формирования' },
|
||||||
|
[idStateUnknown]: { icon: WarningOutlined, text: 'Неизвестно' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrillingProgram = memo(({ idWell }) => {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState()
|
||||||
|
const [historyVisible, setHistoryVisible] = useState(false)
|
||||||
|
const [editorVisible, setEditorVisible] = useState(false)
|
||||||
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
const [categories, setCategories] = useState([])
|
||||||
|
const [data, setData] = useState({})
|
||||||
|
|
||||||
|
const {
|
||||||
|
idState,
|
||||||
|
permissionToEdit,
|
||||||
|
parts,
|
||||||
|
program,
|
||||||
|
error,
|
||||||
|
} = data
|
||||||
|
|
||||||
|
const stateId = idState ?? idStateUnknown
|
||||||
|
const state = stateString[stateId]
|
||||||
|
const StateIcon = state.icon
|
||||||
|
|
||||||
|
const updateData = useCallback(async () => await invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const data = await DrillingProgramService.getState(idWell)
|
||||||
|
const categories = arrayOrDefault(await DrillingProgramService.getCategories(idWell))
|
||||||
|
setData(data)
|
||||||
|
setCategories(categories.filter(cat => {
|
||||||
|
if (cat?.id && (cat.name || cat.shortName))
|
||||||
|
if (data.parts?.findIndex((val) => val.idFileCategory === cat.id) < 0)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось загрузить название скважины "${idWell}"`
|
||||||
|
), [idWell])
|
||||||
|
|
||||||
|
useEffect(() => updateData(), [updateData])
|
||||||
|
|
||||||
|
const onCategoryEdit = (catId) => {
|
||||||
|
setSelectedCategory(catId)
|
||||||
|
setEditorVisible(!!catId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCategoryHistory = (catId) => {
|
||||||
|
setSelectedCategory(catId)
|
||||||
|
setHistoryVisible(!!catId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditorClosed = useCallback(() => {
|
||||||
|
setEditorVisible(false)
|
||||||
|
updateData()
|
||||||
|
}, [updateData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={showLoader}>
|
||||||
|
<Layout style={{ backgroundColor: 'white' }}>
|
||||||
|
<div className={'drilling_program'}>
|
||||||
|
<div className={'program_header'}>
|
||||||
|
<h3>Программа бурения</h3>
|
||||||
|
{permissionToEdit && (
|
||||||
|
<div>
|
||||||
|
<CategoryAdder
|
||||||
|
idWell={idWell}
|
||||||
|
categories={categories}
|
||||||
|
onUpdate={updateData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'program_content'}>
|
||||||
|
{stateId === idStateReady ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
icon={<FileWordOutlined />}
|
||||||
|
style={{ marginLeft: '10px' }}
|
||||||
|
onClick={() => downloadFile(program)}
|
||||||
|
>
|
||||||
|
{program?.name}
|
||||||
|
</Button>
|
||||||
|
<div className={'m-10'}>Размер: {formatBytes(program?.size)}</div>
|
||||||
|
<div className={'m-10'}>Сформирован: {formatDate(program?.uploadDate)}</div>
|
||||||
|
</>
|
||||||
|
) : stateId === idStateError ? (
|
||||||
|
<>
|
||||||
|
<h3 className={'program_status error'}>
|
||||||
|
<StateIcon className={'m-10'} />
|
||||||
|
{error?.message ?? state.text}
|
||||||
|
</h3>
|
||||||
|
<Button icon={<ReloadOutlined />}>
|
||||||
|
Сбросить статус ошибки
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<h3 className={'program_status'}>
|
||||||
|
<StateIcon className={'m-10'} />
|
||||||
|
{state.text}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{parts?.map?.((part, idx) => part && (
|
||||||
|
<CategoryRender
|
||||||
|
key={`${idx}`}
|
||||||
|
idWell={idWell}
|
||||||
|
partData={part}
|
||||||
|
onEdit={permissionToEdit && onCategoryEdit}
|
||||||
|
onUpdate={updateData}
|
||||||
|
onHistory={onCategoryHistory}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{permissionToEdit && (
|
||||||
|
<>
|
||||||
|
<CategoryEditor
|
||||||
|
idWell={idWell}
|
||||||
|
visible={editorVisible}
|
||||||
|
onClosed={onEditorClosed}
|
||||||
|
category={parts?.find((part) => part.idFileCategory === selectedCategory) ?? {}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CategoryHistory
|
||||||
|
idWell={idWell}
|
||||||
|
idCategory={selectedCategory}
|
||||||
|
onClose={() => setHistoryVisible(false)}
|
||||||
|
visible={historyVisible}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DrillingProgram
|
@ -1,38 +1,40 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useCallback, useState } from 'react'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
import { Card, Form, Input, Button } from 'antd'
|
import { Card, Form, Input, Button } from 'antd'
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { AuthService } from '@api'
|
|
||||||
import { setUser } from '@utils/storage'
|
|
||||||
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 { AuthService } from '@api'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
import logo from '@images/logo_32.png'
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
const logoIcon = <img src={logo} alt={'АСБ'} className={'logo'} width={130} />
|
|
||||||
|
|
||||||
export const Login = memo(() => {
|
export const Login = memo(() => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const location = useLocation()
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
|
||||||
const handleLogin = (formData) => invokeWebApiWrapperAsync(
|
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const user = await AuthService.login(formData)
|
const user = await AuthService.login(formData)
|
||||||
if (!user) throw Error('Неправильный логин или пароль')
|
if (!user) throw Error('Неправильный логин или пароль')
|
||||||
setUser(user)
|
setUser(user)
|
||||||
history.push('well')
|
console.log(location.state?.from)
|
||||||
|
history.push(location.state?.from ?? 'well')
|
||||||
},
|
},
|
||||||
setShowLoader,
|
setShowLoader,
|
||||||
(ex) => ex?.message ?? 'Ошибка входа',
|
(ex) => ex?.message ?? 'Ошибка входа',
|
||||||
'Вход в систему'
|
'Вход в систему'
|
||||||
)
|
), [history, location])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
||||||
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }} extra={logoIcon}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Logo style={{ marginBottom: '10px' }}/>
|
||||||
|
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} 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 />} />
|
||||||
@ -50,6 +52,7 @@ export const Login = memo(() => {
|
|||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -48,6 +48,7 @@ export const InclinometryTable = memo(({ group, visible, onClose }) => {
|
|||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
scroll={tableScroll}
|
scroll={tableScroll}
|
||||||
bordered
|
bordered
|
||||||
|
tableName={'measure_inclinometry'}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -11,14 +11,14 @@ import {
|
|||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { MeasureService } from '@api'
|
import { hasPermission } from '@utils/permissions'
|
||||||
import { formatDate } from '@utils'
|
import { formatDate } from '@utils'
|
||||||
|
import { MeasureService } from '@api'
|
||||||
|
|
||||||
import { View } from './View'
|
import { View } from './View'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
import '@styles/measure.css'
|
import '@styles/measure.css'
|
||||||
import { hasPermission } from '@asb/utils/permissions'
|
|
||||||
|
|
||||||
const createEditingColumns = (cols, renderDelegate) =>
|
const createEditingColumns = (cols, renderDelegate) =>
|
||||||
cols.map(col => ({ render: renderDelegate, ...col }))
|
cols.map(col => ({ render: renderDelegate, ...col }))
|
||||||
@ -138,20 +138,20 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona
|
|||||||
|
|
||||||
<div className={'measure-dates mt-20px'}>
|
<div className={'measure-dates mt-20px'}>
|
||||||
<Timeline className={'mt-12px ml-10px'}>
|
<Timeline className={'mt-12px ml-10px'}>
|
||||||
{data.map((item, index) =>
|
{data.map((item, index) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
||||||
key={index}
|
key={index}
|
||||||
className={'measure-button'}
|
className={'measure-button'}
|
||||||
onClick={() => setDisplayedValues(item)}
|
onClick={() => setDisplayedValues(item)}
|
||||||
dot={item?.id === displayedValues?.id &&
|
dot={item?.id === displayedValues?.id && (
|
||||||
<CheckSquareOutlined className={'timeline-clock-icon'} />
|
<CheckSquareOutlined className={'timeline-clock-icon'} />
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}>
|
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}>
|
||||||
{formatDate(item.timestamp, true) ?? 'Нет данных'}
|
{formatDate(item.timestamp) ?? 'Нет данных'}
|
||||||
</span>
|
</span>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
)}
|
))}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,6 +136,7 @@ export const Messages = memo(({ idWell }) => {
|
|||||||
onChange: (page) => setPage(page)
|
onChange: (page) => setPage(page)
|
||||||
}}
|
}}
|
||||||
rowKey={(record) => record.id}
|
rowKey={(record) => record.id}
|
||||||
|
tableName={'messages'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
</>
|
</>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useCallback, useState } from 'react'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
import { Link, useHistory } from 'react-router-dom'
|
||||||
import { Card, Form, Input, Button } from 'antd'
|
import { Card, Form, Input, Button } from 'antd'
|
||||||
import {
|
import {
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
phoneRules
|
phoneRules
|
||||||
} from '@utils/validationRules'
|
} from '@utils/validationRules'
|
||||||
|
|
||||||
import logo from '@images/logo_32.png'
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
const surnameRules = [...nameRules, { required: true, message: 'Пожалуйста, введите фамилию!' }]
|
const surnameRules = [...nameRules, { required: true, message: 'Пожалуйста, введите фамилию!' }]
|
||||||
const regEmailRules = [{ required: true, message: 'Пожалуйста, введите email!' }, ...emailRules]
|
const regEmailRules = [{ required: true, message: 'Пожалуйста, введите email!' }, ...emailRules]
|
||||||
@ -37,7 +37,7 @@ const confirmPasswordRules = [
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
const logoIcon = <img src={logo} alt={'АСБ'} className={'logo'} width={130}/>
|
const logoIcon = <Logo width={130} />
|
||||||
const showPasswordIcon = visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)
|
const showPasswordIcon = visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)
|
||||||
|
|
||||||
const createInput = (name, placeholder, rules, isPassword, dependencies) => (
|
const createInput = (name, placeholder, rules, isPassword, dependencies) => (
|
||||||
@ -54,7 +54,7 @@ export const Register = memo(() => {
|
|||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
const handleRegister = (formData) => invokeWebApiWrapperAsync(
|
const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await AuthService.register(formData)
|
await AuthService.register(formData)
|
||||||
history.push('/login')
|
history.push('/login')
|
||||||
@ -62,7 +62,7 @@ export const Register = memo(() => {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Ошибка отправки заявки на регистрацию`,
|
`Ошибка отправки заявки на регистрацию`,
|
||||||
'Отправка заявки на регистрацию'
|
'Отправка заявки на регистрацию'
|
||||||
)
|
), [history])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
||||||
|
@ -78,6 +78,7 @@ export const Reports = memo(({ idWell }) => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={reports}
|
dataSource={reports}
|
||||||
pagination={{ pageSize: 13 }}
|
pagination={{ pageSize: 13 }}
|
||||||
|
tableName={'reports'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import 'moment/locale/ru'
|
import 'moment/locale/ru'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
import { DatePicker, Radio, Button, Select, notification } from 'antd'
|
import { Radio, Button, Select, notification } from 'antd'
|
||||||
|
|
||||||
import { ReportService } from '@api'
|
import { ReportService } from '@api'
|
||||||
import { Subscribe } from '@services/signalr'
|
import { Subscribe } from '@services/signalr'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import { LoaderPortal } from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { DateRangeWrapper } from 'components/Table/DateRangeWrapper'
|
||||||
|
|
||||||
import { Reports } from './Reports'
|
import { Reports } from './Reports'
|
||||||
import { ReportCreationNotify } from './ReportCreationNotify'
|
import { ReportCreationNotify } from './ReportCreationNotify'
|
||||||
@ -133,11 +134,9 @@ export const Report = memo(({ idWell }) => {
|
|||||||
<div className={'w-100 mt-20px mb-20px d-flex'}>
|
<div className={'w-100 mt-20px mb-20px d-flex'}>
|
||||||
<div>
|
<div>
|
||||||
<div>Диапазон дат отчета</div>
|
<div>Диапазон дат отчета</div>
|
||||||
<DatePicker.RangePicker
|
<DateRangeWrapper
|
||||||
disabledDate={disabledDate}
|
disabledDate={disabledDate}
|
||||||
allowClear={false}
|
|
||||||
onCalendarChange={setFilterDateRange}
|
onCalendarChange={setFilterDateRange}
|
||||||
showTime
|
|
||||||
value={filterDateRange}
|
value={filterDateRange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Table } from 'antd'
|
import { Table } from 'antd'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback, memo } from 'react'
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
@ -10,14 +10,14 @@ import { columns } from '@pages/Messages'
|
|||||||
|
|
||||||
import '@styles/message.css'
|
import '@styles/message.css'
|
||||||
|
|
||||||
export const ActiveMessagesOnline = ({ idWell }) => {
|
export const ActiveMessagesOnline = memo(({ idWell }) => {
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
const [loader, setLoader] = useState(false)
|
const [loader, setLoader] = useState(false)
|
||||||
|
|
||||||
const handleReceiveMessages = (messages) => {
|
const handleReceiveMessages = useCallback((messages) => {
|
||||||
if (messages)
|
if (messages)
|
||||||
setMessages(messages.items.splice(0, 4))
|
setMessages(messages.items.splice(0, 4))
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
@ -30,7 +30,7 @@ export const ActiveMessagesOnline = ({ idWell }) => {
|
|||||||
'Получение списка сообщений'
|
'Получение списка сообщений'
|
||||||
)
|
)
|
||||||
return Subscribe('hubs/telemetry','ReceiveMessages', `well_${idWell}`, handleReceiveMessages)
|
return Subscribe('hubs/telemetry','ReceiveMessages', `well_${idWell}`, handleReceiveMessages)
|
||||||
}, [idWell])
|
}, [idWell, handleReceiveMessages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={loader}>
|
<LoaderPortal show={loader}>
|
||||||
@ -46,6 +46,6 @@ export const ActiveMessagesOnline = ({ idWell }) => {
|
|||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default ActiveMessagesOnline
|
export default ActiveMessagesOnline
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
import { Popover } from 'antd'
|
import { Popover } from 'antd'
|
||||||
import { ControlOutlined } from '@ant-design/icons'
|
import { ControlOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { ValueDisplay } from '@components/Display'
|
import { ValueDisplay } from '@components/Display'
|
||||||
|
|
||||||
export const ChartTimeOnlineFooter = ({ data, lineGroup }) => {
|
export const ChartTimeOnlineFooter = memo(({ data, lineGroup }) => {
|
||||||
const getFooterData = (name) => {
|
const getFooterData = (name) => {
|
||||||
const dataIdx = data && lineGroup?.find(line => line?.footer === name)?.xAccessorName
|
const dataIdx = data && lineGroup?.find(line => line?.footer === name)?.xAccessorName
|
||||||
return (<ValueDisplay value={data?.[dataIdx]}/>)
|
return (<ValueDisplay value={data?.[dataIdx]}/>)
|
||||||
@ -39,4 +40,6 @@ export const ChartTimeOnlineFooter = ({ data, lineGroup }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
export default ChartTimeOnlineFooter
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { makeDateSorter } from '@components/Table'
|
import { makeDateSorter } from '@components/Table'
|
||||||
@ -9,7 +9,7 @@ import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter'
|
|||||||
const GetLimitShape = (flowChartData, points, accessor) => {
|
const GetLimitShape = (flowChartData, points, accessor) => {
|
||||||
const min = [], max = []
|
const min = [], max = []
|
||||||
|
|
||||||
for (let point of points) {
|
for (const point of points) {
|
||||||
const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd)
|
const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd)
|
||||||
if (!program) continue
|
if (!program) continue
|
||||||
|
|
||||||
@ -29,7 +29,9 @@ const RemoveSimilar = (input, accessor) => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, pointCount = 2048, additionalLabels }) => {
|
const addPointData = (point) => ({ depth: point.wellDepth })
|
||||||
|
|
||||||
|
export const MonitoringColumn = memo(({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, pointCount = 2048, additionalLabels }) => {
|
||||||
const [dataStore, setDataStore] = useState([])
|
const [dataStore, setDataStore] = useState([])
|
||||||
const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([])
|
const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([])
|
||||||
const dataLast = data?.[data.length - 1]
|
const dataLast = data?.[data.length - 1]
|
||||||
@ -41,9 +43,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
|||||||
value: dataLast?.[line.xAccessorName]
|
value: dataLast?.[line.xAccessorName]
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const addPointData = (point) => ({ depth: point.wellDepth })
|
const postParsing = useCallback((data) => {
|
||||||
|
|
||||||
const postParsing = (data) => {
|
|
||||||
lineGroupWithoutShapes.forEach(lineCfg => {
|
lineGroupWithoutShapes.forEach(lineCfg => {
|
||||||
const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg)
|
const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg)
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}, [lineGroupWithoutShapes, flowChartData, lineGroup])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDataStore(prevData => {
|
setDataStore(prevData => {
|
||||||
@ -119,6 +119,6 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
|||||||
<ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} />
|
<ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default MonitoringColumn
|
export default MonitoringColumn
|
||||||
|
@ -78,14 +78,17 @@ export const Setpoints = ({ idWell, ...other }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Modal
|
||||||
width={1200}
|
width={1200}
|
||||||
title={'Рекомендованные уставки'}
|
title={(
|
||||||
visible={isModalVisible}
|
<>
|
||||||
onCancel={() => setIsModalVisible(false)}
|
<span style={{ marginRight: '15px' }}>Рекомендованные уставки</span>
|
||||||
footer={(
|
|
||||||
<Button onClick={() => setIsSenderVisible(true)} disabled={!hasPermission('Setpoints.edit')}>
|
<Button onClick={() => setIsSenderVisible(true)} disabled={!hasPermission('Setpoints.edit')}>
|
||||||
Рекомендовать
|
Рекомендовать
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
visible={isModalVisible}
|
||||||
|
onCancel={() => setIsModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
>
|
>
|
||||||
<LoaderPortal show={isLoading}>
|
<LoaderPortal show={isLoading}>
|
||||||
<Table
|
<Table
|
||||||
@ -94,6 +97,7 @@ export const Setpoints = ({ idWell, ...other }) => {
|
|||||||
columns={historyColumns}
|
columns={historyColumns}
|
||||||
dataSource={setpoints}
|
dataSource={setpoints}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Select } from 'antd'
|
import { Select } from 'antd'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DrillFlowChartService,
|
DrillFlowChartService,
|
||||||
@ -49,7 +49,8 @@ const blockHeightGroup = [
|
|||||||
yAccessorName: 'date',
|
yAccessorName: 'date',
|
||||||
color: '#333',
|
color: '#333',
|
||||||
showLine: false,
|
showLine: false,
|
||||||
xConstValue: 30,
|
showDatalabels: true,
|
||||||
|
xConstValue: 50,
|
||||||
dash
|
dash
|
||||||
}, {
|
}, {
|
||||||
label: 'Расход',
|
label: 'Расход',
|
||||||
@ -310,19 +311,15 @@ export default function TelemetryView({ idWell }) {
|
|||||||
const [flowChartData, setFlowChartData] = useState([])
|
const [flowChartData, setFlowChartData] = useState([])
|
||||||
const [rop, setRop] = useState(null)
|
const [rop, setRop] = useState(null)
|
||||||
|
|
||||||
const handleDataSaub = (data) => {
|
const handleDataSaub = useCallback((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const dataSaub = normalizeData(data)
|
const dataSaub = normalizeData(data)
|
||||||
dataSaub.sort(makeDateSorter('date'))
|
dataSaub.sort(makeDateSorter('date'))
|
||||||
setDataSaub(dataSaub)
|
setDataSaub(dataSaub)
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const handleDataSpin = (data) => {
|
const handleDataSpin = useCallback((data) => data && setDataSpin(data), [])
|
||||||
if (data) {
|
|
||||||
setDataSpin(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
|
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
|
||||||
@ -344,7 +341,7 @@ export default function TelemetryView({ idWell }) {
|
|||||||
unsubscribeSaub()
|
unsubscribeSaub()
|
||||||
unsubscribeSpin()
|
unsubscribeSpin()
|
||||||
}
|
}
|
||||||
}, [idWell, chartInterval])
|
}, [idWell, chartInterval, handleDataSpin, handleDataSaub])
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -358,8 +355,7 @@ export default function TelemetryView({ idWell }) {
|
|||||||
'Получение данных по скважине'
|
'Получение данных по скважине'
|
||||||
), [idWell])
|
), [idWell])
|
||||||
|
|
||||||
const onStatusChanged = (value) => {
|
const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
|
||||||
invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
async () => {
|
||||||
const well = { ...wellData, idState: value }
|
const well = { ...wellData, idState: value }
|
||||||
await WellService.updateWell(idWell, well)
|
await WellService.updateWell(idWell, well)
|
||||||
@ -368,8 +364,7 @@ export default function TelemetryView({ idWell }) {
|
|||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось задать состояние скважины "${idWell}"`,
|
`Не удалось задать состояние скважины "${idWell}"`,
|
||||||
'Задание состояния скважины'
|
'Задание состояния скважины'
|
||||||
)
|
), [idWell, wellData])
|
||||||
}
|
|
||||||
|
|
||||||
const columnAdditionalLabels = {
|
const columnAdditionalLabels = {
|
||||||
1: rop && [
|
1: rop && [
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
FilePdfOutlined,
|
FilePdfOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
ExperimentOutlined,
|
ExperimentOutlined,
|
||||||
FundProjectionScreenOutlined,
|
DeploymentUnitOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Layout, Menu } from 'antd'
|
import { Layout, Menu } from 'antd'
|
||||||
import { Switch, useParams } from 'react-router-dom'
|
import { Switch, useParams } from 'react-router-dom'
|
||||||
@ -17,12 +17,15 @@ import Report from './Report'
|
|||||||
import Archive from './Archive'
|
import Archive from './Archive'
|
||||||
import Measure from './Measure'
|
import Measure from './Measure'
|
||||||
import Messages from './Messages'
|
import Messages from './Messages'
|
||||||
|
import Analytics from './Analytics'
|
||||||
import Documents from './Documents'
|
import Documents from './Documents'
|
||||||
import TelemetryView from './TelemetryView'
|
import TelemetryView from './TelemetryView'
|
||||||
import WellOperations from './WellOperations'
|
import WellOperations from './WellOperations'
|
||||||
import DrillingProgram from './DrillingProgram'
|
import DrillingProgram from './DrillingProgram'
|
||||||
import TelemetryAnalysis from './TelemetryAnalysis'
|
import TelemetryAnalysis from './TelemetryAnalysis'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
export const Well = memo(() => {
|
export const Well = memo(() => {
|
||||||
@ -35,6 +38,7 @@ export const Well = memo(() => {
|
|||||||
<PrivateMenuItem.Link root={rootPath} key={'telemetry'} path={'telemetry'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
|
<PrivateMenuItem.Link root={rootPath} key={'telemetry'} path={'telemetry'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'message'} path={'message'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
<PrivateMenuItem.Link root={rootPath} key={'message'} path={'message'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'report'} path={'report'} icon={<FilePdfOutlined />} title={'Рапорт'} />
|
<PrivateMenuItem.Link root={rootPath} key={'report'} path={'report'} icon={<FilePdfOutlined />} title={'Рапорт'} />
|
||||||
|
<PrivateMenuItem.Link root={rootPath} key={'analytics'} path={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} />
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'operations'} path={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} />
|
<PrivateMenuItem.Link root={rootPath} key={'operations'} path={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} />
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||||
{/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */}
|
{/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */}
|
||||||
@ -55,6 +59,9 @@ export const Well = memo(() => {
|
|||||||
<PrivateRoute path={`${rootPath}/report`}>
|
<PrivateRoute path={`${rootPath}/report`}>
|
||||||
<Report idWell={idWell} />
|
<Report idWell={idWell} />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
|
<PrivateRoute path={`${rootPath}/analytics/:tab?`}>
|
||||||
|
<Analytics idWell={idWell} rootPath={`${rootPath}/analytics`}/>
|
||||||
|
</PrivateRoute>
|
||||||
<PrivateRoute path={`${rootPath}/operations/:tab?`}>
|
<PrivateRoute path={`${rootPath}/operations/:tab?`}>
|
||||||
<WellOperations idWell={idWell} />
|
<WellOperations idWell={idWell} />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
@ -77,6 +84,7 @@ export const Well = memo(() => {
|
|||||||
`${rootPath}/telemetry`,
|
`${rootPath}/telemetry`,
|
||||||
`${rootPath}/message`,
|
`${rootPath}/message`,
|
||||||
`${rootPath}/report`,
|
`${rootPath}/report`,
|
||||||
|
`${rootPath}/analytics`,
|
||||||
`${rootPath}/operations`,
|
`${rootPath}/operations`,
|
||||||
`${rootPath}/archive`,
|
`${rootPath}/archive`,
|
||||||
`${rootPath}/telemetryAnalysis`,
|
`${rootPath}/telemetryAnalysis`,
|
||||||
|
@ -62,6 +62,7 @@ export const DrillProcessFlow = memo(({ idWell }) => {
|
|||||||
bordered
|
bordered
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={flows}
|
dataSource={flows}
|
||||||
|
tableName={'well_operations_flow'}
|
||||||
onRowAdd={hasPermission('DrillFlowChart.edit') && onAdd}
|
onRowAdd={hasPermission('DrillFlowChart.edit') && onAdd}
|
||||||
onRowEdit={hasPermission('DrillFlowChart.edit') && onEdit}
|
onRowEdit={hasPermission('DrillFlowChart.edit') && onEdit}
|
||||||
onRowDelete={hasPermission('DrillFlowChart.delete') && onDelete}
|
onRowDelete={hasPermission('DrillFlowChart.delete') && onDelete}
|
||||||
|
@ -21,7 +21,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<Tooltip title={'Импорт - загрузить файл с операциями на сервер'}>
|
<Tooltip title={'Импорт - загрузить файл с операциями на сервер'}>
|
||||||
<Button
|
<Button
|
||||||
disabled={!hasPermission('WellOperation.edit') || disabled}
|
disabled={!hasPermission('WellOperation.edit') || disabled}
|
||||||
@ -50,7 +50,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
|
|||||||
>
|
>
|
||||||
<ImportOperations idWell={idWell} onDone={onDone} />
|
<ImportOperations idWell={idWell} onDone={onDone} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -16,8 +16,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'
|
|||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { getOperations } from '@utils/functions'
|
||||||
import { getOperations } from '@pages/Cluster/functions'
|
|
||||||
|
|
||||||
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
|
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ export const WellDrillParams = memo(({ idWell }) => {
|
|||||||
bordered
|
bordered
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={params}
|
dataSource={params}
|
||||||
|
tableName={'well_drill_params'}
|
||||||
onRowAdd={hasPermission('DrillParams.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление режима бурения')}
|
onRowAdd={hasPermission('DrillParams.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление режима бурения')}
|
||||||
onRowEdit={hasPermission('DrillParams.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование режима бурения')}
|
onRowEdit={hasPermission('DrillParams.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование режима бурения')}
|
||||||
onRowDelete={hasPermission('DrillParams.delete') && makeActionHandler('delete', handlerProps, recordParser, 'Удаление режима бурения')}
|
onRowDelete={hasPermission('DrillParams.delete') && makeActionHandler('delete', handlerProps, recordParser, 'Удаление режима бурения')}
|
||||||
|
@ -69,9 +69,10 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
|
|||||||
sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] }))
|
sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] }))
|
||||||
|
|
||||||
setColumns(preColumns => {
|
setColumns(preColumns => {
|
||||||
preColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
|
const newColumns = [...preColumns]
|
||||||
preColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
|
newColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
|
||||||
return preColumns
|
newColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
|
||||||
|
return newColumns
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setShowLoader,
|
setShowLoader,
|
||||||
@ -131,6 +132,7 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
|
|||||||
total: paginationTotal,
|
total: paginationTotal,
|
||||||
onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
|
onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
|
||||||
}}
|
}}
|
||||||
|
tableName={'well_operationse_editor'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -63,6 +63,7 @@ export const WellSectionsStat = memo(({ idWell }) => {
|
|||||||
size={'small'}
|
size={'small'}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={sections}
|
dataSource={sections}
|
||||||
|
tableName={'well_operations_sections'}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import { memo } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { Layout, Menu } from 'antd'
|
import { Layout, Menu } from 'antd'
|
||||||
import { Switch, useParams, useHistory } from 'react-router-dom'
|
import { Switch, useParams, useHistory, useLocation } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
BuildOutlined,
|
BuildOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
DeploymentUnitOutlined,
|
|
||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
TableOutlined,
|
TableOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
@ -17,32 +16,36 @@ import { ImportExportBar } from './ImportExportBar'
|
|||||||
import { WellDrillParams } from './WellDrillParams'
|
import { WellDrillParams } from './WellDrillParams'
|
||||||
import { DrillProcessFlow } from './DrillProcessFlow'
|
import { DrillProcessFlow } from './DrillProcessFlow'
|
||||||
import { WellSectionsStat } from './WellSectionsStat'
|
import { WellSectionsStat } from './WellSectionsStat'
|
||||||
import { WellCompositeEditor } from './WellCompositeEditor'
|
|
||||||
import { WellOperationsEditor } from './WellOperationsEditor'
|
import { WellOperationsEditor } from './WellOperationsEditor'
|
||||||
|
import { Flex } from '@asb/components/Grid'
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
export const WellOperations = memo(({ idWell }) => {
|
export const WellOperations = memo(({ idWell }) => {
|
||||||
const { tab } = useParams()
|
const { tab } = useParams()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
const location = useLocation()
|
||||||
const rootPath = `/well/${idWell}/operations`
|
const rootPath = `/well/${idWell}/operations`
|
||||||
|
|
||||||
const onImported = () => history.push(`${rootPath}`)
|
const onImported = useCallback(() =>
|
||||||
|
history.push({ pathname: `${rootPath}`, state: { from: location.pathname }})
|
||||||
|
, [history, location, rootPath])
|
||||||
|
|
||||||
const isIEBarDisabled = !['plan', 'fact'].includes(tab)
|
const isIEBarDisabled = !['plan', 'fact'].includes(tab)
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<>
|
<>
|
||||||
<Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}>
|
<Flex style={{ width: '100%' }}>
|
||||||
|
<Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]} style={{ flex: 1 }}>
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<LineChartOutlined />} key={'tvd'} path={'tvd'} title={'TVD'} />
|
<PrivateMenuItemLink root={rootPath} icon={<LineChartOutlined />} key={'tvd'} path={'tvd'} title={'TVD'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<BuildOutlined />} key={'sections'} path={'sections'} title={'Секции'} />
|
<PrivateMenuItemLink root={rootPath} icon={<BuildOutlined />} key={'sections'} path={'sections'} title={'Секции'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'plan'} path={'plan'} title={'План'} />
|
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'plan'} path={'plan'} title={'План'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'fact'} path={'fact'} title={'Факт'} />
|
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'fact'} path={'fact'} title={'Факт'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<BarChartOutlined />} key={'drillProcessFlow'} path={'drillProcessFlow'} title={'РТК'} />
|
<PrivateMenuItemLink root={rootPath} icon={<BarChartOutlined />} key={'drillProcessFlow'} path={'drillProcessFlow'} title={'РТК'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<ControlOutlined />} key={'params'} path={'params'} title={'Режимы'} />
|
<PrivateMenuItemLink root={rootPath} icon={<ControlOutlined />} key={'params'} path={'params'} title={'Режимы'} />
|
||||||
<PrivateMenuItemLink root={rootPath} icon={<DeploymentUnitOutlined />} key={'composite'} path={'composite'} title={'Аналитика'} />
|
|
||||||
<ImportExportBar idWell={idWell} disabled={isIEBarDisabled} onImported={onImported}/>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
|
<ImportExportBar idWell={idWell} disabled={isIEBarDisabled} onImported={onImported}/>
|
||||||
|
</Flex>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Content className={'site-layout-background'}>
|
<Content className={'site-layout-background'}>
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -53,7 +56,7 @@ export const WellOperations = memo(({ idWell }) => {
|
|||||||
<WellSectionsStat idWell={idWell}/>
|
<WellSectionsStat idWell={idWell}/>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
<PrivateRoute path={`${rootPath}/plan`}>
|
<PrivateRoute path={`${rootPath}/plan`}>
|
||||||
<WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'} showSettingsChanger />
|
<WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'}/>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
<PrivateRoute path={`${rootPath}/fact`}>
|
<PrivateRoute path={`${rootPath}/fact`}>
|
||||||
<WellOperationsEditor idWell={idWell} idType={1} tableName={'well_operations_fact'}/>
|
<WellOperationsEditor idWell={idWell} idType={1} tableName={'well_operations_fact'}/>
|
||||||
@ -64,17 +67,13 @@ export const WellOperations = memo(({ idWell }) => {
|
|||||||
<PrivateRoute path={`${rootPath}/params`}>
|
<PrivateRoute path={`${rootPath}/params`}>
|
||||||
<WellDrillParams idWell={idWell}/>
|
<WellDrillParams idWell={idWell}/>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
|
|
||||||
<WellCompositeEditor idWell={idWell}/>
|
|
||||||
</PrivateRoute>
|
|
||||||
<PrivateDefaultRoute urls={[
|
<PrivateDefaultRoute urls={[
|
||||||
`${rootPath}/plan`,
|
`${rootPath}/plan`,
|
||||||
`${rootPath}/fact`,
|
`${rootPath}/fact`,
|
||||||
`${rootPath}/tvd`,
|
`${rootPath}/tvd`,
|
||||||
`${rootPath}/sections`,
|
`${rootPath}/sections`,
|
||||||
`${rootPath}/drillProcessFlow`,
|
`${rootPath}/drillProcessFlow`,
|
||||||
`${rootPath}/params`,
|
`${rootPath}/params`
|
||||||
`${rootPath}/composite`
|
|
||||||
]}/>
|
]}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const reportWebVitals = onPerfEntry => {
|
export const reportWebVitals = onPerfEntry => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getCLS(onPerfEntry);
|
getCLS(onPerfEntry)
|
||||||
getFID(onPerfEntry);
|
getFID(onPerfEntry)
|
||||||
getFCP(onPerfEntry);
|
getFCP(onPerfEntry)
|
||||||
getLCP(onPerfEntry);
|
getLCP(onPerfEntry)
|
||||||
getTTFB(onPerfEntry);
|
getTTFB(onPerfEntry)
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
export default reportWebVitals
|
||||||
|
@ -14,7 +14,20 @@
|
|||||||
//@layout-header-background: rgb(195, 40,40);
|
//@layout-header-background: rgb(195, 40,40);
|
||||||
@layout-header-background: rgb(65, 63, 61);
|
@layout-header-background: rgb(65, 63, 61);
|
||||||
|
|
||||||
#root, .app{min-height:100%;}
|
@header-height: 64px;
|
||||||
|
@layout-min-height: calc(100vh - @header-height);
|
||||||
|
|
||||||
|
#root, .app{
|
||||||
|
min-height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout{
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .ant-menu {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -37,6 +50,7 @@ html {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
background-color: #9d9d9d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow{
|
.shadow{
|
||||||
@ -48,15 +62,13 @@ html {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
gap: 50px;
|
gap: 50px;
|
||||||
|
height: @header-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .logo {
|
.header .logo {
|
||||||
background-color: rgb(230, 230, 230);
|
|
||||||
border-radius: 32px;
|
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
box-shadow: 0 0 2px #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .title{
|
.header .title{
|
||||||
@ -100,9 +112,15 @@ html {
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-layout-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.sheet{
|
.sheet{
|
||||||
padding: 5px 24px;
|
padding: 5px 24px;
|
||||||
min-height: 280px;
|
min-height: calc(@layout-min-height - 15px); // 280px;
|
||||||
margin: 0 15px 15px 15px;
|
margin: 0 15px 15px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
163
src/styles/drilling_program.less
Normal file
163
src/styles/drilling_program.less
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
@border-style: 1px solid #f0f0f0;
|
||||||
|
@header-bg: #fafafa;
|
||||||
|
@content-bg: white;
|
||||||
|
|
||||||
|
@approve-users-flex-width: 6;
|
||||||
|
@approve-panel-flex-width: 3;
|
||||||
|
|
||||||
|
.ml-15 { margin-left: 15px; }
|
||||||
|
.mv-5 { margin: 5px 0; }
|
||||||
|
.m-10 { margin: 10px; }
|
||||||
|
|
||||||
|
.drilling_category {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: @border-style;
|
||||||
|
|
||||||
|
> .category_header {
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: @border-style;
|
||||||
|
background-color: @header-bg;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div > * {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .category_content {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background-color: @content-bg;
|
||||||
|
.drilling-category-column();
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file_column {
|
||||||
|
> .file_info {
|
||||||
|
flex: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .file_actions {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.approve_column {
|
||||||
|
> .user_list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: @approve-users-flex-width;
|
||||||
|
|
||||||
|
> span { margin: 5px 10px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
> .approve_list,
|
||||||
|
> .reject_list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: @approve-panel-flex-width;
|
||||||
|
margin: 0 5px;
|
||||||
|
|
||||||
|
> .panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
> .panel_title {
|
||||||
|
margin: 5px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .panel_content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .approve_list > .panel {
|
||||||
|
background-color: #EFFEEF;
|
||||||
|
border: 1px solid #1FB448;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .reject_list > .panel {
|
||||||
|
background-color: #FEF2EF;
|
||||||
|
border: 1px solid #B4661F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history_approve_column {
|
||||||
|
.approve_column();
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category_adder {
|
||||||
|
display: flex;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
> .adder_select {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drilling_program {
|
||||||
|
border: @border-style;
|
||||||
|
border-top: 0;
|
||||||
|
|
||||||
|
> .program_header {
|
||||||
|
.drilling_category > .category_header();
|
||||||
|
}
|
||||||
|
|
||||||
|
> .program_content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
.program_status {
|
||||||
|
margin: 10px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.program_status.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Миксин для столбцов сетки (размер, отступы, границы) */
|
||||||
|
.drilling-category-column(@first: 4, @last: 8, @padding: 5px 10px) {
|
||||||
|
padding: @padding;
|
||||||
|
&:first-child { flex: @first; } // Относительная ширина первого столбца
|
||||||
|
&:last-child { flex: @last; } // Относительная ширина второго столбца
|
||||||
|
&:not(:last-child) { border-right: @border-style; }
|
||||||
|
}
|
@ -72,7 +72,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h-100vh {
|
.h-100vh {
|
||||||
height: 100vh;
|
height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-10 {
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-align-center {
|
.vertical-align-center {
|
||||||
@ -115,3 +119,11 @@ code {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.first-column-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
25
src/styles/statistics.less
Normal file
25
src/styles/statistics.less
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.statistics-page {
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
.well-selector {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .well-selector-label {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compare-table {
|
||||||
|
.high-efficienty {
|
||||||
|
color: limegreen;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-efficienty {
|
||||||
|
color: red;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
|
import { SimpleTimezoneDto } from '@api'
|
||||||
|
|
||||||
export type RawDate = number | string | Date
|
export type RawDate = number | string | Date
|
||||||
|
|
||||||
export const defaultFormat: string = 'YYYY.MM.DD HH:mm'
|
export const defaultFormat: string = 'YYYY.MM.DD HH:mm'
|
||||||
@ -38,7 +40,40 @@ export const periodToString = (time?: number) => {
|
|||||||
return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
|
return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calcDuration = (start: RawDate, end: RawDate) => {
|
export const calcDuration = (start: unknown, end: unknown) => {
|
||||||
if (!isRawDate(start) || !isRawDate(end)) return
|
if (!isRawDate(start) || !isRawDate(end)) return
|
||||||
return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day
|
return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate => {
|
||||||
|
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return NaN
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setMilliseconds(d.getMilliseconds() + value * timeInS[type] * 1000)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rawTimezones = {
|
||||||
|
'Калининград': 2,
|
||||||
|
'Москва': 3,
|
||||||
|
'Самара': 4,
|
||||||
|
'Екатеринбург': 5,
|
||||||
|
'Омск': 6,
|
||||||
|
'Красноярск': 7,
|
||||||
|
'Новосибирск': 7,
|
||||||
|
'Иркутск': 8,
|
||||||
|
'Чита': 9,
|
||||||
|
'Владивосток': 10,
|
||||||
|
'Магадан': 11,
|
||||||
|
'Южно-Сахалинск': 11,
|
||||||
|
'Среднеколымск': 11,
|
||||||
|
'Анадырь': 12,
|
||||||
|
'Петропавловск-Камчатский': 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimezoneId = keyof typeof rawTimezones
|
||||||
|
|
||||||
|
export const isTimezoneId = (value: unknown): value is TimezoneId => !!value && String(value) in rawTimezones
|
||||||
|
|
||||||
|
export const findTimezoneId = (value: SimpleTimezoneDto): TimezoneId =>
|
||||||
|
(isTimezoneId(value.timezoneId) && value.timezoneId) ||
|
||||||
|
(Object.keys(rawTimezones) as TimezoneId[]).find(id => rawTimezones[id] === value.hours) as TimezoneId
|
||||||
|
86
src/utils/functions.tsx
Normal file
86
src/utils/functions.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { OperationStatService, WellOperationDto, WellOperationDtoPlanFactPredictBase } from '@api'
|
||||||
|
|
||||||
|
const maxPrefix = 'isMax'
|
||||||
|
const minPrefix = 'isMin'
|
||||||
|
|
||||||
|
export const getPrecision = (number: number) => Number.isFinite(number) ? number.toFixed(2) : '-'
|
||||||
|
|
||||||
|
export type KeyType = number | string
|
||||||
|
|
||||||
|
export type SaubData = {
|
||||||
|
key?: number
|
||||||
|
depth?: number
|
||||||
|
date?: string
|
||||||
|
day?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOperations = async (idWell: number): Promise<{
|
||||||
|
operations?: WellOperationDtoPlanFactPredictBase[],
|
||||||
|
plan?: SaubData[]
|
||||||
|
fact?: SaubData[]
|
||||||
|
predict?: SaubData[]
|
||||||
|
}> => {
|
||||||
|
const ops = await OperationStatService.getTvd(idWell)
|
||||||
|
|
||||||
|
if (!ops) return {}
|
||||||
|
|
||||||
|
const convert = (operation?: WellOperationDto): SaubData => ({
|
||||||
|
key: operation?.id,
|
||||||
|
depth: operation?.depthStart,
|
||||||
|
date: operation?.dateStart,
|
||||||
|
day: operation?.day,
|
||||||
|
})
|
||||||
|
|
||||||
|
const planData = ops
|
||||||
|
.map(item => convert(item.plan))
|
||||||
|
.filter(el => el.key)
|
||||||
|
|
||||||
|
const factData = ops
|
||||||
|
.map(item => convert(item.fact))
|
||||||
|
.filter(el => el.key)
|
||||||
|
|
||||||
|
const predictData = ops
|
||||||
|
.map(item => convert(item.predict))
|
||||||
|
.filter(el => el.key)
|
||||||
|
|
||||||
|
return { operations: ops, plan: planData, fact: factData, predict: predictData }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeFilterMinMaxFunction = <T extends unknown>(key: KeyType) => (filterValue: string, dataItem: Record<KeyType, T>) => {
|
||||||
|
if (filterValue === 'max') return dataItem[maxPrefix + key]
|
||||||
|
if (filterValue === 'min') return dataItem[minPrefix + key]
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calcAndUpdateStats = (data: Record<KeyType, number | boolean>[], keys: KeyType[]) => {
|
||||||
|
const mins: Record<KeyType, number | boolean> = {}
|
||||||
|
const maxs: Record<KeyType, number | boolean> = {}
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
maxs[key] = Number.MIN_VALUE
|
||||||
|
mins[key] = Number.MAX_VALUE
|
||||||
|
})
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (mins[key] > item[key]) mins[key] = item[key]
|
||||||
|
if (maxs[key] < item[key]) maxs[key] = item[key]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
keys.forEach((key) => {
|
||||||
|
data[i][maxPrefix + key] = data[i][key] === maxs[key]
|
||||||
|
data[i][minPrefix + key] = data[i][key] === mins[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calcAndUpdateStatsBySections = (data: { sectionType: any }[], keys: KeyType[]) => {
|
||||||
|
const sectionTypes = new Set()
|
||||||
|
data.forEach((item) => sectionTypes.add(item.sectionType))
|
||||||
|
sectionTypes.forEach(sectionType => {
|
||||||
|
const filteredBySectionData = data.filter((item) => item.sectionType === sectionType)
|
||||||
|
calcAndUpdateStats(filteredBySectionData, keys)
|
||||||
|
})
|
||||||
|
}
|
@ -9,3 +9,10 @@ export const mainFrameSize = () => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
|
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Объединить типы, исключив совпадающие поля справа
|
||||||
|
* @param T Тип, передаваемый полностью
|
||||||
|
* @param R Аддитивный тип
|
||||||
|
*/
|
||||||
|
export type OmitExtends<T, R> = T & Omit<R, keyof T>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user