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-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"rxjs": "^7.5.4",
|
||||
"typescript": "^4.2.3",
|
||||
"web-vitals": "^1.1.1"
|
||||
},
|
||||
@ -34,7 +35,7 @@
|
||||
"react_test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://46.146.209.148:89/",
|
||||
"proxy": "http://46.146.209.148:89",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"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 locale from 'antd/lib/locale/ru_RU'
|
||||
|
||||
import { OpenAPI } from './services/api'
|
||||
import { getUserToken } from './utils/storage'
|
||||
import { PrivateRoute } from './components/Private'
|
||||
import { OpenAPI } from '@api'
|
||||
import { getUserToken } from '@utils/storage'
|
||||
import { PrivateRoute } from '@components/Private'
|
||||
|
||||
import Main from './pages/Main'
|
||||
import Login from './pages/Login'
|
||||
import Register from './pages/Register'
|
||||
import Main from '@pages/Main'
|
||||
import Login from '@pages/Login'
|
||||
import Register from '@pages/Register'
|
||||
|
||||
import './styles/App.less'
|
||||
import '@styles/App.less'
|
||||
import { memo } from 'react'
|
||||
|
||||
//OpenAPI.BASE = 'http://localhost:3000'
|
||||
OpenAPI.TOKEN = async () => getUserToken()
|
||||
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
||||
|
||||
export const App = () => (
|
||||
export const App = memo(() => (
|
||||
<ConfigProvider locale={locale}>
|
||||
<Router>
|
||||
<Switch>
|
||||
@ -36,6 +37,6 @@ export const App = () => (
|
||||
</Switch>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
)
|
||||
))
|
||||
|
||||
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 { Link } from 'react-router-dom'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Button, Layout, LayoutProps } from 'antd'
|
||||
|
||||
import PageHeader from '@components/PageHeader'
|
||||
@ -8,17 +8,21 @@ export type AdminLayoutPortalProps = LayoutProps & {
|
||||
title?: ReactNode
|
||||
}
|
||||
|
||||
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => (
|
||||
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<Layout.Content>
|
||||
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
|
||||
<Button size={'large'}>
|
||||
<Link to={'/'}>Вернуться на сайт</Link>
|
||||
<Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Layout>
|
||||
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
||||
</Layout>
|
||||
</Layout.Content>
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
export default AdminLayoutPortal
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { HTMLAttributes } from 'react'
|
||||
|
||||
import Loader from './Loader'
|
||||
import { Loader } from '@components/icons'
|
||||
|
||||
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
|
||||
show?: boolean,
|
||||
fade?: boolean,
|
||||
spinnerProps?: HTMLAttributes<HTMLDivElement>,
|
||||
}
|
||||
|
||||
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ show, fade = true, children, ...other }) => (
|
||||
<div className={'loader-container'} {...other}>
|
||||
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className, show, fade = true, children, spinnerProps, ...other }) => (
|
||||
<div className={`loader-container ${className}`} {...other}>
|
||||
<div className={'loader-content'}>{children}</div>
|
||||
{show && fade && <div className={'loader-fade'}/>}
|
||||
{show && <div className={'loader-overlay'}><Loader/></div>}
|
||||
{show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { memo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
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 { UserMenu } from './UserMenu'
|
||||
import { BasicProps } from 'antd/lib/layout/layout'
|
||||
|
||||
import Logo from '@images/Logo'
|
||||
|
||||
export type PageHeaderProps = BasicProps & {
|
||||
title?: string
|
||||
@ -13,17 +14,21 @@ export type PageHeaderProps = BasicProps & {
|
||||
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.Header className={'header'} {...other}>
|
||||
<Link to={'/'} style={{ height: headerHeight }}>
|
||||
<img src={logo} alt={'АСБ'} className={'logo'}/>
|
||||
<Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}>
|
||||
<Logo />
|
||||
</Link>
|
||||
{children}
|
||||
<h1 className={'title'}>{title}</h1>
|
||||
<UserMenu isAdmin={isAdmin} />
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
export type PrivateContentProps = {
|
||||
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
|
||||
)
|
||||
|
||||
export default PrivateContent
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { isURLAvailable } from '@utils/permissions'
|
||||
@ -9,10 +9,17 @@ export type PrivateDefaultRouteProps = RouteProps & {
|
||||
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={'/'}>
|
||||
<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>
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
export default PrivateDefaultRoute
|
||||
|
@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
|
||||
import { memo, NamedExoticComponent } from 'react'
|
||||
|
||||
import { isURLAvailable } from '@utils/permissions'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
export type PrivateMenuItemProps = MenuItemProps & {
|
||||
root: string
|
||||
@ -16,11 +16,14 @@ export type PrivateMenuLinkProps = MenuItemProps & {
|
||||
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}>
|
||||
<Link to={join(root, path)}>{title}</Link>
|
||||
<Link to={{ pathname: join(root, path), state: { from: location.pathname }}}>{title}</Link>
|
||||
</PrivateMenuItem>
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
||||
Link: NamedExoticComponent<PrivateMenuLinkProps>
|
||||
|
@ -14,7 +14,7 @@ export type PrivateRouteProps = RouteProps & {
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
|
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 { NamePath, Rule } from 'rc-field-form/lib/interface'
|
||||
|
||||
type EditableCellProps = {
|
||||
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
|
||||
editing?: boolean
|
||||
dataIndex?: NamePath
|
||||
input?: React.Component
|
||||
@ -13,7 +14,7 @@ type EditableCellProps = {
|
||||
initialValue: any
|
||||
}
|
||||
|
||||
export const EditableCell = ({
|
||||
export const EditableCell = memo<EditableCellProps>(({
|
||||
editing,
|
||||
dataIndex,
|
||||
input,
|
||||
@ -22,8 +23,9 @@ export const EditableCell = ({
|
||||
formItemRules,
|
||||
children,
|
||||
initialValue,
|
||||
}: EditableCellProps) => (
|
||||
<td style={editing ? { padding: 0 } : undefined}>
|
||||
...other
|
||||
}) => (
|
||||
<td style={editing ? { padding: 0 } : undefined} {...other}>
|
||||
{!editing ? children : (
|
||||
<Form.Item
|
||||
name={dataIndex}
|
||||
@ -39,4 +41,4 @@ export const EditableCell = ({
|
||||
</Form.Item>
|
||||
)}
|
||||
</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 { useState, useEffect } from 'react'
|
||||
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import { Table } from '.'
|
||||
import { EditableCell } from './EditableCell'
|
||||
|
||||
const newRowKeyValue = 'newRow'
|
||||
@ -42,7 +43,7 @@ export const tryAddKeys = (items) => {
|
||||
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
|
||||
}
|
||||
|
||||
export const EditableTable = ({
|
||||
export const EditableTable = memo(({
|
||||
columns,
|
||||
dataSource,
|
||||
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
|
||||
@ -62,14 +63,14 @@ export const EditableTable = ({
|
||||
setData(tryAddKeys(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})
|
||||
setEditingKey(record.key)
|
||||
}
|
||||
}, [form])
|
||||
|
||||
const cancel = () => {
|
||||
const cancel = useCallback(() => {
|
||||
if (editingKey === newRowKeyValue) {
|
||||
const newData = [...data]
|
||||
const index = newData.findIndex((item) => newRowKeyValue === item.key)
|
||||
@ -77,9 +78,9 @@ export const EditableTable = ({
|
||||
setData(newData)
|
||||
}
|
||||
setEditingKey('')
|
||||
}
|
||||
}, [data, editingKey])
|
||||
|
||||
const addNewRow = async () => {
|
||||
const addNewRow = useCallback(async () => {
|
||||
let newRow = {
|
||||
...form.initialValues,
|
||||
key:newRowKeyValue
|
||||
@ -88,9 +89,9 @@ export const EditableTable = ({
|
||||
const newData = [newRow, ...data]
|
||||
setData(newData)
|
||||
edit(newRow)
|
||||
}
|
||||
}, [data, edit, form.initialValues])
|
||||
|
||||
const save = async (record) => {
|
||||
const save = useCallback(async (record) => {
|
||||
try {
|
||||
const row = await form.validateFields()
|
||||
const newData = [...data]
|
||||
@ -121,8 +122,7 @@ export const EditableTable = ({
|
||||
}
|
||||
|
||||
try {
|
||||
if (onChange)
|
||||
onChange(newData)
|
||||
onChange?.(newData)
|
||||
} catch (err) {
|
||||
console.log('callback onChange fault:', err)
|
||||
}
|
||||
@ -130,9 +130,9 @@ export const EditableTable = ({
|
||||
} catch (errInfo) {
|
||||
console.log('Validate Failed:', errInfo)
|
||||
}
|
||||
}
|
||||
}, [data, editingKey, form, onChange, onRowAdd, onRowEdit])
|
||||
|
||||
const deleteRow = (record) =>{
|
||||
const deleteRow = useCallback((record) => {
|
||||
const newData = [...data]
|
||||
const index = newData.findIndex((item) => record.key === item.key)
|
||||
|
||||
@ -140,10 +140,8 @@ export const EditableTable = ({
|
||||
setData(newData)
|
||||
|
||||
onRowDelete(record)
|
||||
|
||||
if (onChange)
|
||||
onChange(newData)
|
||||
}
|
||||
onChange?.(newData)
|
||||
}, [data, onChange, onRowDelete])
|
||||
|
||||
const operationColumn = {
|
||||
width: buttonsWidth ?? 82,
|
||||
@ -180,7 +178,7 @@ export const EditableTable = ({
|
||||
),
|
||||
}
|
||||
|
||||
const handleColumn = (col) => {
|
||||
const handleColumn = useCallback((col) => {
|
||||
if (col.children)
|
||||
col.children = col.children.map(handleColumn)
|
||||
|
||||
@ -197,13 +195,13 @@ export const EditableTable = ({
|
||||
input: col.input,
|
||||
isRequired: col.isRequired,
|
||||
title: col.title,
|
||||
dataType: col.dataType,
|
||||
datatype: col.datatype,
|
||||
formItemClass: col.formItemClass,
|
||||
formItemRules: col.formItemRules,
|
||||
initialValue: col.initialValue,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
const mergedColumns = [...columns.map(handleColumn), operationColumn]
|
||||
|
||||
@ -221,4 +219,4 @@ export const EditableTable = ({
|
||||
/>
|
||||
</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 { EditableTable, makeActionHandler } from './EditableTable'
|
||||
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 = {
|
||||
defaultPageSize: 14,
|
||||
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 = {
|
||||
skip?: number
|
||||
take?: number
|
||||
@ -301,12 +52,3 @@ export const makePaginationObject = (сontainer: PaginationContainer, ...other:
|
||||
total: сontainer.count ?? сontainer.items?.length ?? 0,
|
||||
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) 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) => {
|
||||
const date = new Date(a[key])
|
||||
|
||||
if (Number.isNaN(date.getTime()))
|
||||
export const makeDateSorter = <T extends unknown>(key: keyof DataType<T>) => (a: DataType<T>, b: DataType<T>) => {
|
||||
const adate = a[key]
|
||||
const bdate = b[key]
|
||||
if (!isRawDate(adate) || !isRawDate(bdate))
|
||||
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 { UploadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { upload } from './factory'
|
||||
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 handleFileSend = async () => {
|
||||
const handleFileSend = useCallback(async () => {
|
||||
onUploadStart?.()
|
||||
try {
|
||||
const formDataLocal = new FormData()
|
||||
@ -27,12 +27,14 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
||||
onUploadSuccess?.()
|
||||
}
|
||||
} catch(error) {
|
||||
if(process.env.NODE_ENV === 'development')
|
||||
console.error(error)
|
||||
onUploadError?.(error)
|
||||
} finally {
|
||||
setfileList([])
|
||||
onUploadComplete?.()
|
||||
}
|
||||
}
|
||||
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
|
||||
|
||||
const isSendButtonEnabled = fileList.length > 0
|
||||
return(
|
||||
@ -40,10 +42,11 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
||||
<Upload
|
||||
name={'file'}
|
||||
accept={accept}
|
||||
disabled={disabled}
|
||||
fileList={fileList}
|
||||
onChange={(props) => setfileList(props.fileList)}
|
||||
>
|
||||
<Button icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
type={'primary'}
|
||||
@ -56,3 +59,5 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default UploadForm
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { memo, MouseEventHandler, useState } from 'react'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
import { Button, Dropdown, DropDownProps, Menu } from 'antd'
|
||||
import { UserOutlined } from '@ant-design/icons'
|
||||
|
||||
@ -14,16 +14,17 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
||||
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
const onChangePasswordClick: MouseEventHandler = (e) => {
|
||||
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
|
||||
setIsModalVisible(true)
|
||||
e.preventDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onChangePasswordOk = () => {
|
||||
const onChangePasswordOk = useCallback(() => {
|
||||
setIsModalVisible(false)
|
||||
history.push('/login')
|
||||
}
|
||||
history.push({ pathname: '/login', state: { from: location.pathname }})
|
||||
}, [history, location])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -33,15 +34,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
||||
overlay={(
|
||||
<Menu style={{ textAlign: 'right' }}>
|
||||
{isAdmin ? (
|
||||
<PrivateMenuItemLink path={'/'} title={'Вернуться на сайт'}/>
|
||||
<PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/>
|
||||
) : (
|
||||
<PrivateMenuItemLink path={'/admin'} title={'Панель администратора'}/>
|
||||
<PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/>
|
||||
)}
|
||||
<Menu.Item>
|
||||
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to={'/login'} onClick={removeUser}>Выход</Link>
|
||||
<Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
|
||||
</Menu.Item>
|
||||
</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 { DefaultValueType } from 'rc-tree-select/lib/interface'
|
||||
import { useState, useEffect, ReactNode } from 'react'
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom'
|
||||
import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
|
||||
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 LoaderPortal from './LoaderPortal'
|
||||
@ -54,11 +57,12 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
||||
return value
|
||||
}
|
||||
|
||||
export const WellTreeSelector = () => {
|
||||
const [wellsTree, setWellsTree] = useState<any[]>([]) // TODO: Исправить тип (необходимо разобраться с типом value rc-select)
|
||||
export const WellTreeSelector = memo(({ ...other }) => {
|
||||
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
|
||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||
const [value, setValue] = useState<string>()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
const routeMatch = useRouteMatch('/:route/:id')
|
||||
|
||||
useEffect(() => {
|
||||
@ -100,14 +104,15 @@ export const WellTreeSelector = () => {
|
||||
setValue(getLabel(wellsTree, routeMatch?.url))
|
||||
}, [wellsTree, routeMatch])
|
||||
|
||||
const onChange = (value: string): void => {
|
||||
const onChange = useCallback((value: string): void => {
|
||||
if (wellsTree)
|
||||
setValue(getLabel(wellsTree, value))
|
||||
}
|
||||
}, [wellsTree])
|
||||
|
||||
const onSelect = (value: string): void => {
|
||||
if (value) history.push(value)
|
||||
}
|
||||
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
|
||||
if (['number', 'string'].includes(typeof value))
|
||||
history.push({ pathname: String(value), state: { from: location.pathname }})
|
||||
}, [history, location])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader}>
|
||||
@ -123,9 +128,10 @@ export const WellTreeSelector = () => {
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
style={{ width: '350px' }}
|
||||
{...other}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default WellTreeSelector
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Chart,
|
||||
TimeScale,
|
||||
@ -178,7 +178,7 @@ export const timeParamsByInterval = (intervalSec: number): TimeParams => {
|
||||
return { unit, stepSize }
|
||||
}
|
||||
|
||||
export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams}) => {
|
||||
export const ChartTimeBase = memo<ChartTimeBaseProps>(({ options, dataParams }) => {
|
||||
const chartRef = useRef<HTMLCanvasElement>(null)
|
||||
const [chart, setChart] = useState<any>()
|
||||
|
||||
@ -217,4 +217,4 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams
|
||||
}, [chart, dataParams])
|
||||
|
||||
return(<canvas ref={chartRef} />)
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ChartOptions, Scriptable, ScriptableContext } from 'chart.js'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { makeDateSorter } from '../Table'
|
||||
|
||||
import { makeDateSorter } from '@components/Table'
|
||||
import {
|
||||
ChartTimeBase,
|
||||
ChartTimeData,
|
||||
@ -21,7 +22,8 @@ export type ColumnLineConfig = {
|
||||
dash?: Array<number>
|
||||
borderColor?: string
|
||||
backgroundColor?: string
|
||||
borderWidth?: Scriptable<number, ScriptableContext<'radar'>>
|
||||
borderWidth?: Scriptable<number, ScriptableContext<'line'>>
|
||||
showDatalabels?: boolean
|
||||
fill?: string
|
||||
}
|
||||
export type ColumnPostParsing = (data: ChartTimeDataParams) => void
|
||||
@ -44,7 +46,7 @@ const chartPluginsOptions: ChartOptions = {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 4,
|
||||
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)}`,
|
||||
padding: 6,
|
||||
align: 'left',
|
||||
@ -67,14 +69,14 @@ export const GetOrCreateDatasetByLineConfig = (data: ChartTimeData, lineConfig:
|
||||
?? GetRandomColor()
|
||||
|
||||
dataset = {
|
||||
label: lineConfig.label,
|
||||
label: lineConfig.label?.trimEnd() + (lineConfig.showDatalabels ? ' ' : ''),
|
||||
data: [],
|
||||
backgroundColor: lineConfig.backgroundColor ?? color,
|
||||
borderColor: lineConfig.borderColor ?? color,
|
||||
borderWidth: lineConfig.borderWidth ?? 1,
|
||||
borderDash: lineConfig.dash ?? [],
|
||||
showLine: lineConfig.showLine ?? !lineConfig.isShape,
|
||||
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none')
|
||||
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none'),
|
||||
}
|
||||
|
||||
data.datasets.push(dataset)
|
||||
|
@ -36,7 +36,7 @@ type asyncFunction = (...args: any) => Promise<any|void>
|
||||
export const invokeWebApiWrapperAsync = async (
|
||||
funcAsync: asyncFunction,
|
||||
setShowLoader?: Dispatch<SetStateAction<boolean>>,
|
||||
errorNotifyText?: (string | ((ex: unknown) => string)),
|
||||
errorNotifyText?: ReactNode | ((ex: unknown) => ReactNode),
|
||||
actionName?: string,
|
||||
) => {
|
||||
setShowLoader?.(true)
|
||||
|
@ -3,3 +3,4 @@ export type { WellIconColors, WellIconProps, WellIconState } from './WellIcon'
|
||||
|
||||
export { PointerIcon } from './PointerIcon'
|
||||
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 }) => {
|
||||
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )
|
||||
|
||||
const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0
|
||||
const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0
|
||||
|
||||
return role ? (
|
||||
return (
|
||||
<Tooltip
|
||||
overlayInnerStyle={{ width: '400px' }}
|
||||
title={
|
||||
@ -54,7 +56,5 @@ export const RoleView = memo<RoleViewProps>(({ role }) => {
|
||||
>
|
||||
{role.caption}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={'нет данных'}>-</Tooltip>
|
||||
)
|
||||
})
|
||||
|
@ -1,13 +1,11 @@
|
||||
export type { PermissionViewProps } from './PermissionView'
|
||||
export type { TelemetryViewProps } from './TelemetryView'
|
||||
export type { CompanyViewProps } from './CompanyView'
|
||||
export type { MarkViewProps } from './MarkView'
|
||||
export type { RoleViewProps } from './RoleView'
|
||||
export type { UserViewProps } from './UserView'
|
||||
|
||||
export { PermissionView } from './PermissionView'
|
||||
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
||||
export { CompanyView } from './CompanyView'
|
||||
export { MarkView } from './MarkView'
|
||||
export { RoleView } from './RoleView'
|
||||
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 ReactDOM from 'react-dom';
|
||||
import './styles/index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
|
||||
ReactDOM.render(
|
||||
import '@styles/index.css'
|
||||
|
||||
ReactDOM.render((
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
</React.StrictMode>
|
||||
), document.getElementById('root'))
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
reportWebVitals()
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { memo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export const AccessDenied = memo(() => (
|
||||
<div style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
Доступ запрещён
|
||||
<h2>Доступ запрещён</h2>
|
||||
<Link to={'/login'}>На страницу входа</Link>
|
||||
</div>
|
||||
))
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EditableTable,
|
||||
@ -6,7 +6,8 @@ import {
|
||||
makeSelectColumn,
|
||||
makeActionHandler,
|
||||
makeStringSorter,
|
||||
defaultPagination
|
||||
defaultPagination,
|
||||
makeTimezoneColumn
|
||||
} from '@components/Table'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
@ -17,7 +18,7 @@ import { hasPermission } from '@utils/permissions'
|
||||
|
||||
import { coordsFixed } from './DepositController'
|
||||
|
||||
export const ClusterController = () => {
|
||||
export const ClusterController = memo(() => {
|
||||
const [deposits, setDeposits] = useState([])
|
||||
const [clusters, setClusters] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
@ -36,9 +37,10 @@ export const ClusterController = () => {
|
||||
}),
|
||||
makeColumn('Широта', 'latitude', { 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 () => {
|
||||
const clusters = await AdminClusterService.getAll()
|
||||
setClusters(arrayOrDefault(clusters))
|
||||
@ -46,7 +48,7 @@ export const ClusterController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список кустов`,
|
||||
'Получение списка кустов'
|
||||
)
|
||||
), [])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
@ -59,7 +61,7 @@ export const ClusterController = () => {
|
||||
'Получение списка месторождений'
|
||||
), [])
|
||||
|
||||
useEffect(updateTable, [])
|
||||
useEffect(updateTable, [updateTable])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminClusterService,
|
||||
@ -79,9 +81,10 @@ export const ClusterController = () => {
|
||||
onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')}
|
||||
onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')}
|
||||
onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')}
|
||||
tableName={'admin_cluster_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default ClusterController
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EditableTable,
|
||||
@ -16,15 +16,15 @@ import { min1 } from '@utils/validationRules'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
|
||||
|
||||
export const CompanyController = () => {
|
||||
export const CompanyController = memo(() => {
|
||||
const [columns, setColumns] = useState([])
|
||||
const [companies, setCompanies] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
|
||||
const updateTable = async () => {
|
||||
const updateTable = useCallback(async () => {
|
||||
const companies = await AdminCompanyService.getAll()
|
||||
setCompanies(arrayOrDefault(companies))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async() => {
|
||||
@ -51,7 +51,7 @@ export const CompanyController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список типов компаний`,
|
||||
'Получение списка типов команд'
|
||||
), [])
|
||||
), [updateTable])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminCompanyService,
|
||||
@ -76,9 +76,10 @@ export const CompanyController = () => {
|
||||
onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')}
|
||||
onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')}
|
||||
onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')}
|
||||
tableName={'admin_company_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default CompanyController
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EditableTable,
|
||||
@ -12,7 +12,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { AdminCompanyTypeService } from '@api'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
import { min1 } from '@utils/validationRules'
|
||||
import { hasPermission } from '@asb/utils/permissions'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
|
||||
const columns = [
|
||||
makeColumn('Название', 'caption', {
|
||||
@ -23,11 +23,11 @@ const columns = [
|
||||
}),
|
||||
]
|
||||
|
||||
export const CompanyTypeController = () => {
|
||||
export const CompanyTypeController = memo(() => {
|
||||
const [companyTypes, setCompanyTypes] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
|
||||
const updateTable = () => invokeWebApiWrapperAsync(
|
||||
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async() => {
|
||||
const companyTypes = await AdminCompanyTypeService.getAll()
|
||||
setCompanyTypes(arrayOrDefault(companyTypes))
|
||||
@ -35,9 +35,9 @@ export const CompanyTypeController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список типов компаний`,
|
||||
'Получение списка типов компаний'
|
||||
)
|
||||
), [])
|
||||
|
||||
useEffect(updateTable, [])
|
||||
useEffect(updateTable, [updateTable])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminCompanyTypeService,
|
||||
@ -57,9 +57,10 @@ export const CompanyTypeController = () => {
|
||||
onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')}
|
||||
onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')}
|
||||
onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')}
|
||||
tableName={'admin_company_type_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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 { 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 { arrayOrDefault } from '@utils'
|
||||
import { min1 } from '@utils/validationRules'
|
||||
@ -13,14 +13,15 @@ export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).to
|
||||
const depositColumns = [
|
||||
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
||||
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 [showLoader, setShowLoader] = useState(false)
|
||||
|
||||
const updateTable = () => invokeWebApiWrapperAsync(
|
||||
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async() => {
|
||||
const deposits = await AdminDepositService.getAll()
|
||||
setDeposits(arrayOrDefault(deposits))
|
||||
@ -28,9 +29,9 @@ export const DepositController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список месторождении`,
|
||||
'Получение списка месторождений'
|
||||
)
|
||||
), [])
|
||||
|
||||
useEffect(updateTable, [])
|
||||
useEffect(updateTable, [updateTable])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminDepositService,
|
||||
@ -50,9 +51,10 @@ export const DepositController = () => {
|
||||
onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')}
|
||||
onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')}
|
||||
onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')}
|
||||
tableName={'admin_deposit_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default DepositController
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EditableTable,
|
||||
@ -26,11 +26,11 @@ const columns = [
|
||||
}),
|
||||
]
|
||||
|
||||
export const PermissionController = () => {
|
||||
export const PermissionController = memo(() => {
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [permissions, setPermissions] = useState([])
|
||||
|
||||
const updateTable = async () => invokeWebApiWrapperAsync(
|
||||
const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const permission = await AdminPermissionService.getAll()
|
||||
setPermissions(arrayOrDefault(permission))
|
||||
@ -38,9 +38,9 @@ export const PermissionController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список прав`,
|
||||
'Получение списка прав'
|
||||
)
|
||||
), [])
|
||||
|
||||
useEffect(() => updateTable(), [])
|
||||
useEffect(() => updateTable(), [updateTable])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminPermissionService,
|
||||
@ -60,9 +60,10 @@ export const PermissionController = () => {
|
||||
onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')}
|
||||
onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')}
|
||||
onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')}
|
||||
tableName={'admin_permission_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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 { PermissionView, RoleView } from '@components/views'
|
||||
@ -15,20 +15,21 @@ export const RoleController = memo(() => {
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [columns, setColumns] = useState([])
|
||||
|
||||
const loadRoles = async () => {
|
||||
const loadRoles = useCallback(async () => {
|
||||
const roles = await AdminUserRoleService.getAll()
|
||||
setRoles(arrayOrDefault(roles))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setColumns([
|
||||
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
||||
makeColumn('Название', 'caption', { width: 100, editable: true, formItemRules: min1 }),
|
||||
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
|
||||
width: 200,
|
||||
width: 400,
|
||||
editable: true,
|
||||
render: (role) => <RoleView role={role} />
|
||||
}, { allowClear: true }),
|
||||
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
|
||||
width: 600,
|
||||
editable: true,
|
||||
render: (permission) => <PermissionView info={permission} />,
|
||||
}),
|
||||
@ -44,7 +45,7 @@ export const RoleController = memo(() => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список ролей`,
|
||||
'Получение списка ролей'
|
||||
), [])
|
||||
), [loadRoles])
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminUserRoleService,
|
||||
@ -68,6 +69,7 @@ export const RoleController = memo(() => {
|
||||
onRowAdd={hasPermission('AdminUserRole.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление роли')}
|
||||
onRowEdit={hasPermission('AdminUserRole.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование роли')}
|
||||
onRowDelete={hasPermission('AdminUserRole.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление роли')}
|
||||
tableName={'admin_role_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
defaultPagination,
|
||||
@ -29,7 +29,7 @@ const columns = [
|
||||
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
|
||||
]
|
||||
|
||||
export const TelemetryController = () => {
|
||||
export const TelemetryController = memo(() => {
|
||||
const [telemetryData, setTelemetryData] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
@ -56,9 +56,10 @@ export const TelemetryController = () => {
|
||||
columns={columns}
|
||||
dataSource={telemetryData}
|
||||
pagination={defaultPagination}
|
||||
tableName={'admin_telemetry_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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 { useEffect, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
|
||||
|
||||
import {
|
||||
EditableTable,
|
||||
@ -17,29 +18,71 @@ import { ChangePassword } from '@components/ChangePassword'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
|
||||
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 { arrayOrDefault } from '@utils'
|
||||
|
||||
import RoleTag from './RoleTag'
|
||||
|
||||
const SEARCH_TIMEOUT = 400
|
||||
|
||||
export const UserController = () => {
|
||||
export const UserController = memo(() => {
|
||||
const [users, setUsers] = useState([])
|
||||
const [filteredUsers, setFilteredUsers] = useState([])
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [columns, setColumns] = useState([])
|
||||
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
|
||||
icon={<UserSwitchOutlined />}
|
||||
onClick={() => setSelectedUser(record)}
|
||||
title={'Сменить пароль'}
|
||||
disabled={editingKey !== ''}
|
||||
/>
|
||||
)
|
||||
), [])
|
||||
|
||||
const updateTable = () => invokeWebApiWrapperAsync(
|
||||
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async() => {
|
||||
const users = await AdminUserService.getAll()
|
||||
setUsers(arrayOrDefault(users))
|
||||
@ -47,7 +90,7 @@ export const UserController = () => {
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список пользователей`,
|
||||
'Получение списка пользователей'
|
||||
)
|
||||
), [])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
@ -61,7 +104,7 @@ export const UserController = () => {
|
||||
setUsers(users)
|
||||
|
||||
const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email'])
|
||||
|
||||
const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))]
|
||||
|
||||
setColumns([
|
||||
makeColumn('Логин', 'login', {
|
||||
@ -124,6 +167,8 @@ export const UserController = () => {
|
||||
makeColumn('Роли', 'roleNames', {
|
||||
editable: true,
|
||||
input: <RoleTag roles={roles} />,
|
||||
filters: roleFilters,
|
||||
onFilter: makeArrayOnFilter('roleNames'),
|
||||
render: (item) => item?.map((elm) => (
|
||||
<Tag key={elm} color={'blue'}>
|
||||
<RoleView role={roles.find((role) => role.caption === elm)} />
|
||||
@ -148,20 +193,30 @@ export const UserController = () => {
|
||||
onComplete: updateTable,
|
||||
}
|
||||
|
||||
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder={'Поиск пользователей'}
|
||||
onChange={onSearchTextChange}
|
||||
style={{ marginBottom: '15px' }}
|
||||
loading={isSearching}
|
||||
/>
|
||||
<EditableTable
|
||||
size={'small'}
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
dataSource={filteredUsers}
|
||||
onRowAdd={hasPermission('AdminUser.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление пользователя')}
|
||||
onRowEdit={hasPermission('AdminUser.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование пользователя')}
|
||||
onRowDelete={hasPermission('AdminUser.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление пользователя')}
|
||||
additionalButtons={additionalButtons}
|
||||
buttonsWidth={120}
|
||||
pagination={defaultPagination}
|
||||
tableName={'admin_user_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
<ChangePassword
|
||||
@ -172,6 +227,6 @@ export const UserController = () => {
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default UserController
|
||||
|
@ -41,6 +41,7 @@ export const VisitLog = memo(() => {
|
||||
columns={columns}
|
||||
dataSource={logData}
|
||||
pagination={defaultPagination}
|
||||
tableName={'visit_log'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button } from 'antd'
|
||||
import { CopyOutlined } from '@ant-design/icons'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
AdminClusterService,
|
||||
@ -17,24 +17,30 @@ import {
|
||||
makeNumericSorter,
|
||||
makeTagColumn,
|
||||
defaultPagination,
|
||||
makeTimezoneColumn,
|
||||
} from '@components/Table'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { TelemetryView, CompanyView } from '@components/views'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
|
||||
import { coordsFixed } from '../DepositController'
|
||||
import TelemetrySelect from './TelemetrySelect'
|
||||
|
||||
import '@styles/admin.css'
|
||||
import { hasPermission } from '@asb/utils/permissions'
|
||||
|
||||
const wellTypes = [
|
||||
{ value: 1, label: 'Наклонно-направленная' },
|
||||
{ value: 2, label: 'Горизонтальная' },
|
||||
]
|
||||
|
||||
export const WellController = () => {
|
||||
const recordParser = (record) => ({
|
||||
...record,
|
||||
idTelemetry: record.telemetry?.id,
|
||||
})
|
||||
|
||||
export const WellController = memo(() => {
|
||||
const [columns, setColumns] = useState([])
|
||||
const [wells, setWells] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
@ -53,14 +59,14 @@ export const WellController = () => {
|
||||
// TODO: Метод дубликации скважины
|
||||
}
|
||||
|
||||
const addititonalButtons = (record, editingKey) => (
|
||||
const addititonalButtons = memo((record, editingKey) => (
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
title={'Дублировать скважину'}
|
||||
disabled={(editingKey ?? '') !== ''}
|
||||
onClick={() => duplicateWell(record)}
|
||||
/>
|
||||
)
|
||||
))
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
@ -94,6 +100,7 @@ export const WellController = () => {
|
||||
render: (telemetry) => <TelemetryView telemetry={telemetry} />,
|
||||
input: <TelemetrySelect telemetry={telemetry}/>,
|
||||
}, ),
|
||||
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
|
||||
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
|
||||
editable: true,
|
||||
render: (company) => <CompanyView company={company} />,
|
||||
@ -107,11 +114,6 @@ export const WellController = () => {
|
||||
'Получение списка кустов'
|
||||
), [])
|
||||
|
||||
const recordParser = (record) => ({
|
||||
...record,
|
||||
idTelemetry: record.telemetry?.id,
|
||||
})
|
||||
|
||||
const handlerProps = {
|
||||
service: AdminWellService,
|
||||
setLoader: setShowLoader,
|
||||
@ -132,9 +134,10 @@ export const WellController = () => {
|
||||
onRowDelete={hasPermission('AdminWell.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление скважины')}
|
||||
//additionalButtons={addititonalButtons}
|
||||
buttonsWidth={95}
|
||||
tableName={'admin_well_controller'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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 { useState, useEffect, memo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useState, useEffect, memo, useCallback } from 'react'
|
||||
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
|
||||
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 { DrillParamsService, WellCompositeService } from '@api'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
|
||||
import {
|
||||
calcAndUpdateStatsBySections,
|
||||
makeFilterMinMaxFunction,
|
||||
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 { Tvd } from '../Tvd'
|
||||
import { getColumns } from '../WellDrillParams'
|
||||
|
||||
|
||||
const filtersMinMax = [
|
||||
@ -42,6 +42,8 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
||||
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
|
||||
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
|
||||
|
||||
useEffect(() => {
|
||||
@ -125,7 +127,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
||||
|
||||
const columns = [
|
||||
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 ?? '-'),
|
||||
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
|
||||
@ -187,7 +189,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
||||
)
|
||||
}
|
||||
|
||||
const onParamButtonClick = () => invokeWebApiWrapperAsync(
|
||||
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
setIsParamsModalVisible(true)
|
||||
const params = await DrillParamsService.getCompositeAll(idWell)
|
||||
@ -196,9 +198,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
||||
setShowParamsLoader,
|
||||
`Не удалось загрузить список режимов для скважины "${idWell}"`,
|
||||
'Получение списка режимов скважины'
|
||||
)
|
||||
), [idWell])
|
||||
|
||||
const onParamsAddClick = () => invokeWebApiWrapperAsync(
|
||||
const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await DrillParamsService.save(idWell, params)
|
||||
setIsParamsModalVisible(false)
|
||||
@ -206,7 +208,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
|
||||
setShowLoader,
|
||||
`Не удалось добавить режимы в список скважины "${idWell}"`,
|
||||
'Добавление режима скважины'
|
||||
)
|
||||
), [idWell, params])
|
||||
|
||||
return (
|
||||
<>
|
@ -1,15 +1,14 @@
|
||||
import { useState, useEffect, memo } from 'react'
|
||||
import { Switch, useParams } from 'react-router-dom'
|
||||
import { Col, Layout, Menu, Row, Tag, TreeSelect } from 'antd'
|
||||
import { Col, Layout, Menu, Row } from 'antd'
|
||||
|
||||
import {
|
||||
DepositService,
|
||||
OperationStatService,
|
||||
WellCompositeService,
|
||||
} from '@api'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import WellSelector from '@components/WellSelector'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
|
||||
|
||||
@ -18,45 +17,17 @@ import { WellCompositeSections } from './WellCompositeSections'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
export const WellCompositeEditor = memo(({ idWell }) => {
|
||||
const rootPath = `/well/${idWell}/operations/composite`
|
||||
export const WellCompositeEditor = memo(({ idWell, rootPath }) => {
|
||||
const { tab } = useParams()
|
||||
|
||||
const [wellsTree, setWellsTree] = useState([])
|
||||
const [wellLabels, setWellLabels] = useState([])
|
||||
const [statsWells, setStatsWells] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [showTabLoader, setShowTabLoader] = useState(false)
|
||||
const [selectedIdWells, setSelectedIdWells] = useState([])
|
||||
const [selectedSections, setSelectedSections] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
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 {
|
||||
const selected = await WellCompositeService.get(idWell)
|
||||
setSelectedSections(arrayOrDefault(selected))
|
||||
@ -67,8 +38,7 @@ export const WellCompositeEditor = memo(({ idWell }) => {
|
||||
setShowLoader,
|
||||
'Не удалось загрузить список скважин',
|
||||
'Получение списка скважин'
|
||||
)
|
||||
}, [idWell])
|
||||
), [idWell])
|
||||
|
||||
useEffect(() => {
|
||||
const wellIds = selectedSections.map((value) => value.idWellSrc)
|
||||
@ -87,24 +57,12 @@ export const WellCompositeEditor = memo(({ idWell }) => {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<TreeSelect
|
||||
multiple
|
||||
treeCheckable
|
||||
showCheckedStrategy={TreeSelect.SHOW_CHILD}
|
||||
treeDefaultExpandAll
|
||||
treeData={wellsTree}
|
||||
treeLine={{ showLeafIcon: false }}
|
||||
onChange={(value) => setSelectedIdWells(value)}
|
||||
size={'middle'}
|
||||
style={{ width: '100%' }}
|
||||
<WellSelector
|
||||
idWell={idWell}
|
||||
onChange={setSelectedIdWells}
|
||||
value={selectedIdWells}
|
||||
placeholder={'Выберите скважины'}
|
||||
tagRender={(props) => (
|
||||
<Tag {...props}>{wellLabels[props.value] ?? props.label}</Tag>
|
||||
)}
|
||||
disabled={!hasPermission('WellOperation.edit')}
|
||||
/>
|
||||
</Col>
|
||||
<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 { useState, useEffect } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useState, useEffect, memo } from 'react'
|
||||
import { Tag, Button, Modal } from 'antd'
|
||||
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
|
||||
|
||||
@ -16,13 +16,14 @@ import { CompanyView } from '@components/views'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import PointerIcon from '@components/icons/PointerIcon'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import { Tvd } from '@pages/WellOperations/Tvd'
|
||||
import {
|
||||
getOperations,
|
||||
calcAndUpdateStatsBySections,
|
||||
makeFilterMinMaxFunction
|
||||
} from './functions'
|
||||
} from '@utils/functions'
|
||||
import { isRawDate } from '@utils'
|
||||
|
||||
import { Tvd } from '@pages/WellOperations/Tvd'
|
||||
import WellOperationsTable from './WellOperationsTable'
|
||||
|
||||
const filtersMinMax = [
|
||||
@ -34,7 +35,9 @@ const filtersWellsType = []
|
||||
const DAY_IN_MS = 86_400_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 [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
|
||||
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
|
||||
@ -42,6 +45,8 @@ export const ClusterWells = ({ statsWells }) => {
|
||||
const [tableData, setTableData] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpsModalVisible || selectedWellId <= 0) {
|
||||
setWellOperations([])
|
||||
@ -61,14 +66,11 @@ export const ClusterWells = ({ statsWells }) => {
|
||||
useEffect(() => {
|
||||
let data = statsWells?.map((well) => {
|
||||
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
|
||||
? (new Date(well.total?.plan?.end) - new Date(well.total?.plan?.start)) / DAY_IN_MS
|
||||
: '-'
|
||||
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
|
||||
: '-'
|
||||
const dateOrM = (a, b) => a && b ? (new Date(b) - new Date(a)) / 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)
|
||||
|
||||
return {
|
||||
key: well.caption,
|
||||
@ -106,14 +108,10 @@ export const ClusterWells = ({ statsWells }) => {
|
||||
setTableData(data)
|
||||
}, [statsWells])
|
||||
|
||||
const getDate = (str) => Number.isNaN(+new Date(str)) || +new Date(str) === 0
|
||||
? '-'
|
||||
: new Date(str).toLocaleString()
|
||||
|
||||
const columns = [
|
||||
makeTextColumn('скв №', 'caption', null, null,
|
||||
(_, 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
|
||||
state={item.idState === 1 ? 'active' : 'unknown'}
|
||||
width={32}
|
||||
@ -172,6 +170,7 @@ export const ClusterWells = ({ statsWells }) => {
|
||||
bordered
|
||||
pagination={false}
|
||||
rowKey={(record) => record.caption}
|
||||
tableName={'cluster'}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
@ -199,6 +198,6 @@ export const ClusterWells = ({ statsWells }) => {
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default ClusterWells
|
||||
|
@ -1,9 +1,7 @@
|
||||
|
||||
import { Table } from 'antd'
|
||||
|
||||
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
|
||||
|
||||
import { getPrecision } from './functions'
|
||||
import { getPrecision } from '@utils/functions'
|
||||
|
||||
export const WellOperationsTable = ({ wellOperations }) => {
|
||||
const columns = [
|
||||
@ -27,7 +25,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
|
||||
commentFact: el.fact?.comment ?? '-'
|
||||
}))
|
||||
|
||||
return(
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
size={'small'}
|
||||
@ -35,6 +33,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
|
||||
dataSource={operations}
|
||||
rowKey={(record) => record.key}
|
||||
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 { Link } from 'react-router-dom'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useState, useEffect, memo } from 'react'
|
||||
|
||||
import { ClusterService } from '@api'
|
||||
@ -8,6 +8,8 @@ import { PointerIcon } from '@components/icons'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import '@styles/index.css'
|
||||
|
||||
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
|
||||
|
||||
const calcViewParams = (clusters) => {
|
||||
@ -41,6 +43,8 @@ export const Deposit = memo(() => {
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [viewParams, setViewParams] = useState(defaultViewParams)
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const data = await ClusterService.getClusters()
|
||||
@ -62,7 +66,7 @@ export const Deposit = memo(() => {
|
||||
anchor={[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} />
|
||||
<span>{cluster.caption}</span>
|
||||
</Link>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import moment from 'moment'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { DatePicker, Button, Input } from 'antd'
|
||||
|
||||
@ -9,6 +8,7 @@ import { UploadForm } from '@components/UploadForm'
|
||||
import { CompanyView, UserView } from '@components/views'
|
||||
import { EditableTable, makePaginationObject } from '@components/Table'
|
||||
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
|
||||
import { formatDate } from '@utils'
|
||||
|
||||
const pageSize = 12
|
||||
const { RangePicker } = DatePicker
|
||||
@ -28,7 +28,7 @@ const columns = [
|
||||
title: 'Дата загрузки',
|
||||
key: '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: 'Размер',
|
||||
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 [filterDataRange, setFilterDataRange] = useState([])
|
||||
const [filterCompanyName, setFilterCompanyName] = useState([])
|
||||
@ -173,6 +173,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
|
||||
}}
|
||||
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete}
|
||||
rowKey={(record) => record.id}
|
||||
tableName={tableName ?? `file_${idCategory}`}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -46,7 +46,7 @@ export const MenuDocuments = memo(({ idWell }) => {
|
||||
<Switch>
|
||||
{documentCategories.map(category => (
|
||||
<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>
|
||||
))}
|
||||
<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 { Link, useHistory } from 'react-router-dom'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button } from 'antd'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
|
||||
import { AuthService } from '@api'
|
||||
import { setUser } from '@utils/storage'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { loginRules, passwordRules } from '@utils/validationRules'
|
||||
import { setUser } from '@utils/storage'
|
||||
import { AuthService } from '@api'
|
||||
|
||||
import '@styles/index.css'
|
||||
import logo from '@images/logo_32.png'
|
||||
|
||||
const logoIcon = <img src={logo} alt={'АСБ'} className={'logo'} width={130} />
|
||||
import Logo from '@images/Logo'
|
||||
|
||||
export const Login = memo(() => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
|
||||
const handleLogin = (formData) => invokeWebApiWrapperAsync(
|
||||
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const user = await AuthService.login(formData)
|
||||
if (!user) throw Error('Неправильный логин или пароль')
|
||||
setUser(user)
|
||||
history.push('well')
|
||||
console.log(location.state?.from)
|
||||
history.push(location.state?.from ?? 'well')
|
||||
},
|
||||
setShowLoader,
|
||||
(ex) => ex?.message ?? 'Ошибка входа',
|
||||
'Вход в систему'
|
||||
)
|
||||
), [history, location])
|
||||
|
||||
return (
|
||||
<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.Item name={'login'} rules={loginRules}>
|
||||
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
|
||||
@ -50,6 +52,7 @@ export const Login = memo(() => {
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
)
|
||||
})
|
||||
|
@ -48,6 +48,7 @@ export const InclinometryTable = memo(({ group, visible, onClose }) => {
|
||||
columns={tableColumns}
|
||||
scroll={tableScroll}
|
||||
bordered
|
||||
tableName={'measure_inclinometry'}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -11,14 +11,14 @@ import {
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { MeasureService } from '@api'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
import { formatDate } from '@utils'
|
||||
import { MeasureService } from '@api'
|
||||
|
||||
import { View } from './View'
|
||||
|
||||
import '@styles/index.css'
|
||||
import '@styles/measure.css'
|
||||
import { hasPermission } from '@asb/utils/permissions'
|
||||
|
||||
const createEditingColumns = (cols, renderDelegate) =>
|
||||
cols.map(col => ({ render: renderDelegate, ...col }))
|
||||
@ -138,20 +138,20 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona
|
||||
|
||||
<div className={'measure-dates mt-20px'}>
|
||||
<Timeline className={'mt-12px ml-10px'}>
|
||||
{data.map((item, index) =>
|
||||
{data.map((item, index) => (
|
||||
<Timeline.Item
|
||||
key={index}
|
||||
className={'measure-button'}
|
||||
onClick={() => setDisplayedValues(item)}
|
||||
dot={item?.id === displayedValues?.id &&
|
||||
dot={item?.id === displayedValues?.id && (
|
||||
<CheckSquareOutlined className={'timeline-clock-icon'} />
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}>
|
||||
{formatDate(item.timestamp, true) ?? 'Нет данных'}
|
||||
{formatDate(item.timestamp) ?? 'Нет данных'}
|
||||
</span>
|
||||
</Timeline.Item>
|
||||
)}
|
||||
))}
|
||||
</Timeline>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -136,6 +136,7 @@ export const Messages = memo(({ idWell }) => {
|
||||
onChange: (page) => setPage(page)
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
tableName={'messages'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button } from 'antd'
|
||||
import {
|
||||
@ -21,7 +21,7 @@ import {
|
||||
phoneRules
|
||||
} from '@utils/validationRules'
|
||||
|
||||
import logo from '@images/logo_32.png'
|
||||
import Logo from '@images/Logo'
|
||||
|
||||
const surnameRules = [...nameRules, { required: true, message: 'Пожалуйста, введите фамилию!' }]
|
||||
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 createInput = (name, placeholder, rules, isPassword, dependencies) => (
|
||||
@ -54,7 +54,7 @@ export const Register = memo(() => {
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const history = useHistory()
|
||||
|
||||
const handleRegister = (formData) => invokeWebApiWrapperAsync(
|
||||
const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await AuthService.register(formData)
|
||||
history.push('/login')
|
||||
@ -62,7 +62,7 @@ export const Register = memo(() => {
|
||||
setShowLoader,
|
||||
`Ошибка отправки заявки на регистрацию`,
|
||||
'Отправка заявки на регистрацию'
|
||||
)
|
||||
), [history])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
|
||||
|
@ -78,6 +78,7 @@ export const Reports = memo(({ idWell }) => {
|
||||
columns={columns}
|
||||
dataSource={reports}
|
||||
pagination={{ pageSize: 13 }}
|
||||
tableName={'reports'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -1,12 +1,13 @@
|
||||
import 'moment/locale/ru'
|
||||
import moment from 'moment'
|
||||
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 { Subscribe } from '@services/signalr'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { LoaderPortal } from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DateRangeWrapper } from 'components/Table/DateRangeWrapper'
|
||||
|
||||
import { Reports } from './Reports'
|
||||
import { ReportCreationNotify } from './ReportCreationNotify'
|
||||
@ -133,11 +134,9 @@ export const Report = memo(({ idWell }) => {
|
||||
<div className={'w-100 mt-20px mb-20px d-flex'}>
|
||||
<div>
|
||||
<div>Диапазон дат отчета</div>
|
||||
<DatePicker.RangePicker
|
||||
<DateRangeWrapper
|
||||
disabledDate={disabledDate}
|
||||
allowClear={false}
|
||||
onCalendarChange={setFilterDateRange}
|
||||
showTime
|
||||
value={filterDateRange}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Table } from 'antd'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback, memo } from 'react'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
@ -10,14 +10,14 @@ import { columns } from '@pages/Messages'
|
||||
|
||||
import '@styles/message.css'
|
||||
|
||||
export const ActiveMessagesOnline = ({ idWell }) => {
|
||||
export const ActiveMessagesOnline = memo(({ idWell }) => {
|
||||
const [messages, setMessages] = useState([])
|
||||
const [loader, setLoader] = useState(false)
|
||||
|
||||
const handleReceiveMessages = (messages) => {
|
||||
const handleReceiveMessages = useCallback((messages) => {
|
||||
if (messages)
|
||||
setMessages(messages.items.splice(0, 4))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
@ -30,7 +30,7 @@ export const ActiveMessagesOnline = ({ idWell }) => {
|
||||
'Получение списка сообщений'
|
||||
)
|
||||
return Subscribe('hubs/telemetry','ReceiveMessages', `well_${idWell}`, handleReceiveMessages)
|
||||
}, [idWell])
|
||||
}, [idWell, handleReceiveMessages])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={loader}>
|
||||
@ -46,6 +46,6 @@ export const ActiveMessagesOnline = ({ idWell }) => {
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default ActiveMessagesOnline
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import { ControlOutlined } from '@ant-design/icons'
|
||||
|
||||
import { ValueDisplay } from '@components/Display'
|
||||
|
||||
export const ChartTimeOnlineFooter = ({ data, lineGroup }) => {
|
||||
export const ChartTimeOnlineFooter = memo(({ data, lineGroup }) => {
|
||||
const getFooterData = (name) => {
|
||||
const dataIdx = data && lineGroup?.find(line => line?.footer === name)?.xAccessorName
|
||||
return (<ValueDisplay value={data?.[dataIdx]}/>)
|
||||
@ -39,4 +40,6 @@ export const ChartTimeOnlineFooter = ({ data, lineGroup }) => {
|
||||
</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 { makeDateSorter } from '@components/Table'
|
||||
@ -9,7 +9,7 @@ import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter'
|
||||
const GetLimitShape = (flowChartData, points, accessor) => {
|
||||
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)
|
||||
if (!program) continue
|
||||
|
||||
@ -29,7 +29,9 @@ const RemoveSimilar = (input, accessor) => {
|
||||
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 [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([])
|
||||
const dataLast = data?.[data.length - 1]
|
||||
@ -41,9 +43,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
||||
value: dataLast?.[line.xAccessorName]
|
||||
}))
|
||||
|
||||
const addPointData = (point) => ({ depth: point.wellDepth })
|
||||
|
||||
const postParsing = (data) => {
|
||||
const postParsing = useCallback((data) => {
|
||||
lineGroupWithoutShapes.forEach(lineCfg => {
|
||||
const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg)
|
||||
|
||||
@ -54,7 +54,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [lineGroupWithoutShapes, flowChartData, lineGroup])
|
||||
|
||||
useEffect(() => {
|
||||
setDataStore(prevData => {
|
||||
@ -119,6 +119,6 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
|
||||
<ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default MonitoringColumn
|
||||
|
@ -78,14 +78,17 @@ export const Setpoints = ({ idWell, ...other }) => {
|
||||
</Button>
|
||||
<Modal
|
||||
width={1200}
|
||||
title={'Рекомендованные уставки'}
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={(
|
||||
title={(
|
||||
<>
|
||||
<span style={{ marginRight: '15px' }}>Рекомендованные уставки</span>
|
||||
<Button onClick={() => setIsSenderVisible(true)} disabled={!hasPermission('Setpoints.edit')}>
|
||||
Рекомендовать
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<LoaderPortal show={isLoading}>
|
||||
<Table
|
||||
@ -94,6 +97,7 @@ export const Setpoints = ({ idWell, ...other }) => {
|
||||
columns={historyColumns}
|
||||
dataSource={setpoints}
|
||||
pagination={false}
|
||||
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</Modal>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Select } from 'antd'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
import {
|
||||
DrillFlowChartService,
|
||||
@ -49,7 +49,8 @@ const blockHeightGroup = [
|
||||
yAccessorName: 'date',
|
||||
color: '#333',
|
||||
showLine: false,
|
||||
xConstValue: 30,
|
||||
showDatalabels: true,
|
||||
xConstValue: 50,
|
||||
dash
|
||||
}, {
|
||||
label: 'Расход',
|
||||
@ -310,19 +311,15 @@ export default function TelemetryView({ idWell }) {
|
||||
const [flowChartData, setFlowChartData] = useState([])
|
||||
const [rop, setRop] = useState(null)
|
||||
|
||||
const handleDataSaub = (data) => {
|
||||
const handleDataSaub = useCallback((data) => {
|
||||
if (data) {
|
||||
const dataSaub = normalizeData(data)
|
||||
dataSaub.sort(makeDateSorter('date'))
|
||||
setDataSaub(dataSaub)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDataSpin = (data) => {
|
||||
if (data) {
|
||||
setDataSpin(data)
|
||||
}
|
||||
}
|
||||
const handleDataSpin = useCallback((data) => data && setDataSpin(data), [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
|
||||
@ -344,7 +341,7 @@ export default function TelemetryView({ idWell }) {
|
||||
unsubscribeSaub()
|
||||
unsubscribeSpin()
|
||||
}
|
||||
}, [idWell, chartInterval])
|
||||
}, [idWell, chartInterval, handleDataSpin, handleDataSaub])
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
@ -358,8 +355,7 @@ export default function TelemetryView({ idWell }) {
|
||||
'Получение данных по скважине'
|
||||
), [idWell])
|
||||
|
||||
const onStatusChanged = (value) => {
|
||||
invokeWebApiWrapperAsync(
|
||||
const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const well = { ...wellData, idState: value }
|
||||
await WellService.updateWell(idWell, well)
|
||||
@ -368,8 +364,7 @@ export default function TelemetryView({ idWell }) {
|
||||
setShowLoader,
|
||||
`Не удалось задать состояние скважины "${idWell}"`,
|
||||
'Задание состояния скважины'
|
||||
)
|
||||
}
|
||||
), [idWell, wellData])
|
||||
|
||||
const columnAdditionalLabels = {
|
||||
1: rop && [
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
FilePdfOutlined,
|
||||
DatabaseOutlined,
|
||||
ExperimentOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Layout, Menu } from 'antd'
|
||||
import { Switch, useParams } from 'react-router-dom'
|
||||
@ -17,12 +17,15 @@ import Report from './Report'
|
||||
import Archive from './Archive'
|
||||
import Measure from './Measure'
|
||||
import Messages from './Messages'
|
||||
import Analytics from './Analytics'
|
||||
import Documents from './Documents'
|
||||
import TelemetryView from './TelemetryView'
|
||||
import WellOperations from './WellOperations'
|
||||
import DrillingProgram from './DrillingProgram'
|
||||
import TelemetryAnalysis from './TelemetryAnalysis'
|
||||
|
||||
import '@styles/index.css'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
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={'message'} path={'message'} icon={<AlertOutlined/>} 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={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||
{/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */}
|
||||
@ -55,6 +59,9 @@ export const Well = memo(() => {
|
||||
<PrivateRoute path={`${rootPath}/report`}>
|
||||
<Report idWell={idWell} />
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path={`${rootPath}/analytics/:tab?`}>
|
||||
<Analytics idWell={idWell} rootPath={`${rootPath}/analytics`}/>
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path={`${rootPath}/operations/:tab?`}>
|
||||
<WellOperations idWell={idWell} />
|
||||
</PrivateRoute>
|
||||
@ -77,6 +84,7 @@ export const Well = memo(() => {
|
||||
`${rootPath}/telemetry`,
|
||||
`${rootPath}/message`,
|
||||
`${rootPath}/report`,
|
||||
`${rootPath}/analytics`,
|
||||
`${rootPath}/operations`,
|
||||
`${rootPath}/archive`,
|
||||
`${rootPath}/telemetryAnalysis`,
|
||||
|
@ -62,6 +62,7 @@ export const DrillProcessFlow = memo(({ idWell }) => {
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={flows}
|
||||
tableName={'well_operations_flow'}
|
||||
onRowAdd={hasPermission('DrillFlowChart.edit') && onAdd}
|
||||
onRowEdit={hasPermission('DrillFlowChart.edit') && onEdit}
|
||||
onRowDelete={hasPermission('DrillFlowChart.delete') && onDelete}
|
||||
|
@ -21,7 +21,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Tooltip title={'Импорт - загрузить файл с операциями на сервер'}>
|
||||
<Button
|
||||
disabled={!hasPermission('WellOperation.edit') || disabled}
|
||||
@ -50,7 +50,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
|
||||
>
|
||||
<ImportOperations idWell={idWell} onDone={onDone} />
|
||||
</Modal>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -16,8 +16,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import { getOperations } from '@pages/Cluster/functions'
|
||||
import { getOperations } from '@utils/functions'
|
||||
|
||||
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
|
||||
|
||||
|
@ -71,6 +71,7 @@ export const WellDrillParams = memo(({ idWell }) => {
|
||||
bordered
|
||||
columns={columns}
|
||||
dataSource={params}
|
||||
tableName={'well_drill_params'}
|
||||
onRowAdd={hasPermission('DrillParams.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление режима бурения')}
|
||||
onRowEdit={hasPermission('DrillParams.edit') && makeActionHandler('update', 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] }))
|
||||
|
||||
setColumns(preColumns => {
|
||||
preColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
|
||||
preColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
|
||||
return preColumns
|
||||
const newColumns = [...preColumns]
|
||||
newColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
|
||||
newColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
|
||||
return newColumns
|
||||
})
|
||||
},
|
||||
setShowLoader,
|
||||
@ -131,6 +132,7 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
|
||||
total: paginationTotal,
|
||||
onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
|
||||
}}
|
||||
tableName={'well_operationse_editor'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -63,6 +63,7 @@ export const WellSectionsStat = memo(({ idWell }) => {
|
||||
size={'small'}
|
||||
columns={columns}
|
||||
dataSource={sections}
|
||||
tableName={'well_operations_sections'}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
)
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { Layout, Menu } from 'antd'
|
||||
import { Switch, useParams, useHistory } from 'react-router-dom'
|
||||
import { Switch, useParams, useHistory, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
BarChartOutlined,
|
||||
BuildOutlined,
|
||||
ControlOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
LineChartOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons'
|
||||
@ -17,32 +16,36 @@ import { ImportExportBar } from './ImportExportBar'
|
||||
import { WellDrillParams } from './WellDrillParams'
|
||||
import { DrillProcessFlow } from './DrillProcessFlow'
|
||||
import { WellSectionsStat } from './WellSectionsStat'
|
||||
import { WellCompositeEditor } from './WellCompositeEditor'
|
||||
import { WellOperationsEditor } from './WellOperationsEditor'
|
||||
import { Flex } from '@asb/components/Grid'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
export const WellOperations = memo(({ idWell }) => {
|
||||
const { tab } = useParams()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
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)
|
||||
|
||||
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={<BuildOutlined />} key={'sections'} path={'sections'} 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={<BarChartOutlined />} key={'drillProcessFlow'} path={'drillProcessFlow'} 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>
|
||||
<ImportExportBar idWell={idWell} disabled={isIEBarDisabled} onImported={onImported}/>
|
||||
</Flex>
|
||||
<Layout>
|
||||
<Content className={'site-layout-background'}>
|
||||
<Switch>
|
||||
@ -53,7 +56,7 @@ export const WellOperations = memo(({ idWell }) => {
|
||||
<WellSectionsStat idWell={idWell}/>
|
||||
</PrivateRoute>
|
||||
<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 path={`${rootPath}/fact`}>
|
||||
<WellOperationsEditor idWell={idWell} idType={1} tableName={'well_operations_fact'}/>
|
||||
@ -64,17 +67,13 @@ export const WellOperations = memo(({ idWell }) => {
|
||||
<PrivateRoute path={`${rootPath}/params`}>
|
||||
<WellDrillParams idWell={idWell}/>
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
|
||||
<WellCompositeEditor idWell={idWell}/>
|
||||
</PrivateRoute>
|
||||
<PrivateDefaultRoute urls={[
|
||||
`${rootPath}/plan`,
|
||||
`${rootPath}/fact`,
|
||||
`${rootPath}/tvd`,
|
||||
`${rootPath}/sections`,
|
||||
`${rootPath}/drillProcessFlow`,
|
||||
`${rootPath}/params`,
|
||||
`${rootPath}/composite`
|
||||
`${rootPath}/params`
|
||||
]}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
|
@ -1,13 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
export const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default reportWebVitals;
|
||||
export default reportWebVitals
|
||||
|
@ -14,7 +14,20 @@
|
||||
//@layout-header-background: rgb(195, 40,40);
|
||||
@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 {
|
||||
display: flex;
|
||||
@ -29,14 +42,15 @@ html {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.login_page{
|
||||
.login_page {
|
||||
position: absolute;
|
||||
height:100%;
|
||||
width:100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
background-color: #9d9d9d;
|
||||
}
|
||||
|
||||
.shadow{
|
||||
@ -48,15 +62,13 @@ html {
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: 50px;
|
||||
height: @header-height;
|
||||
}
|
||||
|
||||
.header .logo {
|
||||
background-color: rgb(230, 230, 230);
|
||||
border-radius: 32px;
|
||||
padding: 8px 24px;
|
||||
margin: 0 10px;
|
||||
margin-bottom: 2px;
|
||||
box-shadow: 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.header .title{
|
||||
@ -100,9 +112,15 @@ html {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sheet{
|
||||
padding: 5px 24px;
|
||||
min-height: 280px;
|
||||
min-height: calc(@layout-min-height - 15px); // 280px;
|
||||
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 {
|
||||
height: 100vh;
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.vertical-align-center {
|
||||
@ -115,3 +119,11 @@ code {
|
||||
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 { SimpleTimezoneDto } from '@api'
|
||||
|
||||
export type RawDate = number | string | Date
|
||||
|
||||
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)}`
|
||||
}
|
||||
|
||||
export const calcDuration = (start: RawDate, end: RawDate) => {
|
||||
export const calcDuration = (start: unknown, end: unknown) => {
|
||||
if (!isRawDate(start) || !isRawDate(end)) return
|
||||
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
|
||||
|
||||
/**
|
||||
* Объединить типы, исключив совпадающие поля справа
|
||||
* @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