Merge branch 'dev'

This commit is contained in:
Александр Сироткин 2022-03-14 10:17:14 +05:00
commit 43aec286b3
104 changed files with 29666 additions and 6170 deletions

31982
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"rxjs": "^7.5.4",
"typescript": "^4.2.3", "typescript": "^4.2.3",
"web-vitals": "^1.1.1" "web-vitals": "^1.1.1"
}, },
@ -34,7 +35,7 @@
"react_test": "react-scripts test", "react_test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"proxy": "http://46.146.209.148:89/", "proxy": "http://46.146.209.148:89",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -6,21 +6,22 @@ import {
import { ConfigProvider } from 'antd' import { ConfigProvider } from 'antd'
import locale from 'antd/lib/locale/ru_RU' import locale from 'antd/lib/locale/ru_RU'
import { OpenAPI } from './services/api' import { OpenAPI } from '@api'
import { getUserToken } from './utils/storage' import { getUserToken } from '@utils/storage'
import { PrivateRoute } from './components/Private' import { PrivateRoute } from '@components/Private'
import Main from './pages/Main' import Main from '@pages/Main'
import Login from './pages/Login' import Login from '@pages/Login'
import Register from './pages/Register' import Register from '@pages/Register'
import './styles/App.less' import '@styles/App.less'
import { memo } from 'react'
//OpenAPI.BASE = 'http://localhost:3000' //OpenAPI.BASE = 'http://localhost:3000'
OpenAPI.TOKEN = async () => getUserToken() OpenAPI.TOKEN = async () => getUserToken()
OpenAPI.HEADERS = {'Content-Type': 'application/json'} OpenAPI.HEADERS = {'Content-Type': 'application/json'}
export const App = () => ( export const App = memo(() => (
<ConfigProvider locale={locale}> <ConfigProvider locale={locale}>
<Router> <Router>
<Switch> <Switch>
@ -36,6 +37,6 @@ export const App = () => (
</Switch> </Switch>
</Router> </Router>
</ConfigProvider> </ConfigProvider>
) ))
export default App export default App

View 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

View File

@ -1,5 +1,5 @@
import { memo, ReactNode } from 'react' import { memo, ReactNode } from 'react'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { Button, Layout, LayoutProps } from 'antd' import { Button, Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader' import PageHeader from '@components/PageHeader'
@ -8,17 +8,21 @@ export type AdminLayoutPortalProps = LayoutProps & {
title?: ReactNode title?: ReactNode
} }
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => ( export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
<Layout.Content> const location = useLocation()
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}> return (
<Link to={'/'}>Вернуться на сайт</Link> <Layout.Content>
</Button> <PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
</PageHeader> <Button size={'large'}>
<Layout> <Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link>
<Layout.Content className={'site-layout-background sheet'} {...props}/> </Button>
</Layout> </PageHeader>
</Layout.Content> <Layout>
)) <Layout.Content className={'site-layout-background sheet'} {...props}/>
</Layout>
</Layout.Content>
)
})
export default AdminLayoutPortal export default AdminLayoutPortal

View File

@ -1,17 +1,18 @@
import { HTMLAttributes } from 'react' import { HTMLAttributes } from 'react'
import Loader from './Loader' import { Loader } from '@components/icons'
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & { type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
show?: boolean, show?: boolean,
fade?: boolean, fade?: boolean,
spinnerProps?: HTMLAttributes<HTMLDivElement>,
} }
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ show, fade = true, children, ...other }) => ( export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className, show, fade = true, children, spinnerProps, ...other }) => (
<div className={'loader-container'} {...other}> <div className={`loader-container ${className}`} {...other}>
<div className={'loader-content'}>{children}</div> <div className={'loader-content'}>{children}</div>
{show && fade && <div className={'loader-fade'}/>} {show && fade && <div className={'loader-fade'}/>}
{show && <div className={'loader-overlay'}><Loader/></div>} {show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
</div> </div>
) )

View File

@ -1,11 +1,12 @@
import { memo } from 'react' import { memo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Layout } from 'antd' import { Layout } from 'antd'
import { Link } from 'react-router-dom' import { BasicProps } from 'antd/lib/layout/layout'
import logo from '@images/logo_32.png'
import { headerHeight } from '@utils' import { headerHeight } from '@utils'
import { UserMenu } from './UserMenu' import { UserMenu } from './UserMenu'
import { BasicProps } from 'antd/lib/layout/layout'
import Logo from '@images/Logo'
export type PageHeaderProps = BasicProps & { export type PageHeaderProps = BasicProps & {
title?: string title?: string
@ -13,17 +14,21 @@ export type PageHeaderProps = BasicProps & {
children?: React.ReactNode children?: React.ReactNode
} }
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => ( export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
<Layout> const location = useLocation()
<Layout.Header className={'header'} {...other}>
<Link to={'/'} style={{ height: headerHeight }}> return (
<img src={logo} alt={'АСБ'} className={'logo'}/> <Layout>
</Link> <Layout.Header className={'header'} {...other}>
{children} <Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}>
<h1 className={'title'}>{title}</h1> <Logo />
<UserMenu isAdmin={isAdmin} /> </Link>
</Layout.Header> {children}
</Layout> <h1 className={'title'}>{title}</h1>
)) <UserMenu isAdmin={isAdmin} />
</Layout.Header>
</Layout>
)
})
export default PageHeader export default PageHeader

View 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

View File

@ -1,13 +1,14 @@
import React from 'react' import { memo, ReactElement } from 'react'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils/permissions'
export type PrivateContentProps = { export type PrivateContentProps = {
absolutePath: string absolutePath: string
children?: React.ReactElement<any, any> children?: ReactElement<any, any>
} }
export const PrivateContent: React.FC<PrivateContentProps> = ({ absolutePath, children = null }) => export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
isURLAvailable(absolutePath) ? children : null isURLAvailable(absolutePath) ? children : null
)
export default PrivateContent export default PrivateContent

View File

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

View File

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

View File

@ -14,7 +14,7 @@ export type PrivateRouteProps = RouteProps & {
} }
export const defaultRedirect = (location?: Location<unknown>) => ( export const defaultRedirect = (location?: Location<unknown>) => (
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location } }} /> <Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location?.pathname } }} />
) )
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => { export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => {

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -1,7 +1,8 @@
import { memo } from 'react'
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import { NamePath, Rule } from 'rc-field-form/lib/interface' import { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = { type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
editing?: boolean editing?: boolean
dataIndex?: NamePath dataIndex?: NamePath
input?: React.Component input?: React.Component
@ -13,7 +14,7 @@ type EditableCellProps = {
initialValue: any initialValue: any
} }
export const EditableCell = ({ export const EditableCell = memo<EditableCellProps>(({
editing, editing,
dataIndex, dataIndex,
input, input,
@ -22,8 +23,9 @@ export const EditableCell = ({
formItemRules, formItemRules,
children, children,
initialValue, initialValue,
}: EditableCellProps) => ( ...other
<td style={editing ? { padding: 0 } : undefined}> }) => (
<td style={editing ? { padding: 0 } : undefined} {...other}>
{!editing ? children : ( {!editing ? children : (
<Form.Item <Form.Item
name={dataIndex} name={dataIndex}
@ -39,4 +41,4 @@ export const EditableCell = ({
</Form.Item> </Form.Item>
)} )}
</td> </td>
) ))

View File

@ -1,9 +1,10 @@
import { Form, Table, Button, Popconfirm } from 'antd' import { memo, useCallback, useState, useEffect } from 'react'
import { Form, Button, Popconfirm } from 'antd'
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons' import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
import { useState, useEffect } from 'react'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table } from '.'
import { EditableCell } from './EditableCell' import { EditableCell } from './EditableCell'
const newRowKeyValue = 'newRow' const newRowKeyValue = 'newRow'
@ -42,7 +43,7 @@ export const tryAddKeys = (items) => {
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index })) return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
} }
export const EditableTable = ({ export const EditableTable = memo(({
columns, columns,
dataSource, dataSource,
onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия
@ -62,14 +63,14 @@ export const EditableTable = ({
setData(tryAddKeys(dataSource)) setData(tryAddKeys(dataSource))
}, [dataSource]) }, [dataSource])
const isEditing = (record) => record?.key === editingKey const isEditing = useCallback((record) => record?.key === editingKey, [editingKey])
const edit = (record) => { const edit = useCallback((record) => {
form.setFieldsValue({...record}) form.setFieldsValue({...record})
setEditingKey(record.key) setEditingKey(record.key)
} }, [form])
const cancel = () => { const cancel = useCallback(() => {
if (editingKey === newRowKeyValue) { if (editingKey === newRowKeyValue) {
const newData = [...data] const newData = [...data]
const index = newData.findIndex((item) => newRowKeyValue === item.key) const index = newData.findIndex((item) => newRowKeyValue === item.key)
@ -77,9 +78,9 @@ export const EditableTable = ({
setData(newData) setData(newData)
} }
setEditingKey('') setEditingKey('')
} }, [data, editingKey])
const addNewRow = async () => { const addNewRow = useCallback(async () => {
let newRow = { let newRow = {
...form.initialValues, ...form.initialValues,
key:newRowKeyValue key:newRowKeyValue
@ -88,9 +89,9 @@ export const EditableTable = ({
const newData = [newRow, ...data] const newData = [newRow, ...data]
setData(newData) setData(newData)
edit(newRow) edit(newRow)
} }, [data, edit, form.initialValues])
const save = async (record) => { const save = useCallback(async (record) => {
try { try {
const row = await form.validateFields() const row = await form.validateFields()
const newData = [...data] const newData = [...data]
@ -121,8 +122,7 @@ export const EditableTable = ({
} }
try { try {
if (onChange) onChange?.(newData)
onChange(newData)
} catch (err) { } catch (err) {
console.log('callback onChange fault:', err) console.log('callback onChange fault:', err)
} }
@ -130,9 +130,9 @@ export const EditableTable = ({
} catch (errInfo) { } catch (errInfo) {
console.log('Validate Failed:', errInfo) console.log('Validate Failed:', errInfo)
} }
} }, [data, editingKey, form, onChange, onRowAdd, onRowEdit])
const deleteRow = (record) =>{ const deleteRow = useCallback((record) => {
const newData = [...data] const newData = [...data]
const index = newData.findIndex((item) => record.key === item.key) const index = newData.findIndex((item) => record.key === item.key)
@ -140,10 +140,8 @@ export const EditableTable = ({
setData(newData) setData(newData)
onRowDelete(record) onRowDelete(record)
onChange?.(newData)
if (onChange) }, [data, onChange, onRowDelete])
onChange(newData)
}
const operationColumn = { const operationColumn = {
width: buttonsWidth ?? 82, width: buttonsWidth ?? 82,
@ -180,7 +178,7 @@ export const EditableTable = ({
), ),
} }
const handleColumn = (col) => { const handleColumn = useCallback((col) => {
if (col.children) if (col.children)
col.children = col.children.map(handleColumn) col.children = col.children.map(handleColumn)
@ -197,13 +195,13 @@ export const EditableTable = ({
input: col.input, input: col.input,
isRequired: col.isRequired, isRequired: col.isRequired,
title: col.title, title: col.title,
dataType: col.dataType, datatype: col.datatype,
formItemClass: col.formItemClass, formItemClass: col.formItemClass,
formItemRules: col.formItemRules, formItemRules: col.formItemRules,
initialValue: col.initialValue, initialValue: col.initialValue,
}), }),
} }
} }, [isEditing])
const mergedColumns = [...columns.map(handleColumn), operationColumn] const mergedColumns = [...columns.map(handleColumn), operationColumn]
@ -221,4 +219,4 @@ export const EditableTable = ({
/> />
</Form> </Form>
) )
} })

View 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

View 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

View File

@ -1,312 +1,54 @@
import { memo, useEffect, useState, ReactNode } from 'react'
import { InputNumber, Select, Table as RawTable, Tag, SelectProps, TableProps } from 'antd'
import { SelectValue } from 'antd/lib/select'
import { OptionsType } from 'rc-select/lib/interface'
import { Rule } from 'rc-field-form/lib/interface'
import { tryAddKeys } from './EditableTable'
import { makeNumericSorter, makeStringSorter } from './sorters'
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters' export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
export { EditableTable, makeActionHandler } from './EditableTable' export { EditableTable, makeActionHandler } from './EditableTable'
export { DatePickerWrapper } from './DatePickerWrapper' export { DatePickerWrapper } from './DatePickerWrapper'
export { Table } from './Table'
export {
RegExpIsFloat,
timezoneOptions,
TimezoneSelect,
makeGroupColumn,
makeColumn,
makeColumnsPlanFact,
makeFilterTextMatch,
makeNumericRender,
makeNumericColumn,
makeNumericColumnOptions,
makeNumericColumnPlanFact,
makeNumericStartEnd,
makeNumericMinMax,
makeNumericAvgRange,
makeSelectColumn,
makeTagColumn,
makeTagInput,
makeTextColumn,
makeTimezoneColumn,
makeTimezoneRenderer,
} from './Columns'
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/ export type {
DataType,
RenderMethod,
SorterMethod,
TagInputProps,
columnPropsOther,
} from './Columns'
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
export const defaultPagination = { export const defaultPagination = {
defaultPageSize: 14, defaultPageSize: 14,
showSizeChanger: true, showSizeChanger: true,
}
export const makeNumericRender = (fixed?: number) => (value: any, _: object): ReactNode => {
let val = '-'
if ((value ?? null) !== null && Number.isFinite(+value)) {
val = (fixed ?? null) !== null
? (+value).toFixed(fixed)
: (+value).toPrecision(5)
}
return (
<div className={'text-align-r-container'}>
<span>{val}</span>
</div>
)
}
export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string) => ({
editable: true,
initialValue: 0,
width: 100,
sorter: sorterKey ? makeNumericSorter(sorterKey) : null,
formItemRules: [
{
required: true,
message: 'Введите число',
pattern: RegExpIsFloat,
},
],
render: makeNumericRender(fixed),
})
/*
other - объект с дополнительными свойствами колонки
поддерживаются все базовые свойства из описания https://ant.design/components/table/#Column
плю дополнительные для колонок EditableTable: */
interface columnPropsOther {
// редактируемая колонка
editable?: boolean
// react компонента редактора
input?: ReactNode
// значение может быть пустым
isRequired?: boolean
// css класс для <FormItem/>, если требуется
formItemClass?: string
// массив правил валидации значений https://ant.design/components/form/#Rule
formItemRules?: Rule[]
// дефолтное значение при добавлении новой строки
initialValue?: string | number
render?: (...attributes: any) => any
}
export const makeColumn = (title: string | ReactNode, key: string, other?: columnPropsOther) => ({
title: title,
key: key,
dataIndex: key,
...other,
})
export const makeColumnsPlanFact = (
title: string | ReactNode,
key: string | string[],
columsOther?: any | any[],
gruopOther?: any
) => {
let keyPlanLocal: string
let keyFactLocal: string
if (key instanceof Array) {
keyPlanLocal = key[0]
keyFactLocal = key[1]
} else {
keyPlanLocal = key + 'Plan'
keyFactLocal = key + 'Fact'
}
let columsOtherLocal : any[2]
if (columsOther instanceof Array)
columsOtherLocal = [columsOther[0], columsOther[1]]
else
columsOtherLocal = [columsOther, columsOther]
return {
title: title,
...gruopOther,
children: [
makeColumn('план', keyPlanLocal, columsOtherLocal[0]),
makeColumn('факт', keyFactLocal, columsOtherLocal[1]),
]
}
}
export const makeFilterTextMatch = (key: string | number) => (
(filterValue: string | number, dataItem: any) => dataItem[key] === filterValue
)
export const makeGroupColumn = (title: string, children: object[]) => ({ title, children })
export const makeTextColumn = (
title: string,
dataIndex: string,
filters: object[],
sorter?: (key: string) => any,
render?: any,
other?: any
) => ({
title: title,
dataIndex: dataIndex,
key: dataIndex,
filters: filters,
onFilter: filters ? makeFilterTextMatch(dataIndex) : null,
sorter: sorter ?? makeStringSorter(dataIndex),
render: render,
...other
})
export const makeNumericColumn = (
title: string,
dataIndex: string,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string,
other?: columnPropsOther
) => ({
title: title,
dataIndex: dataIndex,
key: dataIndex,
filters: filters,
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
sorter: makeNumericSorter(dataIndex),
width: width,
input: <InputNumber style={{ width: '100%' }}/>,
render: renderDelegate ?? makeNumericRender(),
align: 'right',
...other
})
export const makeNumericColumnPlanFact = (
title: string,
dataIndex: string,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string
) => makeGroupColumn(title, [
makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width),
makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width),
])
export const makeNumericStartEnd = (
title: string,
dataIndex: string,
fixed: number,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string,
) => makeGroupColumn(title, [
makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')),
makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End'))
])
export const makeNumericMinMax = (
title: string,
dataIndex: string,
fixed: number,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string,
) => makeGroupColumn(title, [
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
])
export const makeNumericAvgRange = (
title: string,
dataIndex: string,
fixed: number,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string
) => makeGroupColumn(title, [
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
])
export const makeSelectColumn = <T extends unknown = string>(
title: string,
dataIndex: string,
options: OptionsType,
defaultValue?: T,
other?: columnPropsOther,
selectOther?: SelectProps<SelectValue>
) => makeColumn(title, dataIndex, {
...other,
input: <Select options={options} {...selectOther}/>,
render: (value) => {
const item = options?.find(option => option?.value === value)
return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--'
}
})
const makeTagInput = <T extends Record<string, any>>(value_key: string, label_key: string) => memo<{
options: T[],
value?: T[],
onChange?: (values: T[]) => void,
other?: SelectProps<SelectValue>,
}>(({ options, value, onChange, other }) => {
const [selectOptions, setSelectOptions] = useState<OptionsType>([])
const [selectedValue, setSelectedValue] = useState<SelectValue>([])
useEffect(() => {
setSelectOptions(options.map((elm) => ({
value: String(elm[value_key]),
label: elm[label_key],
})))
}, [options])
useEffect(() => {
setSelectedValue(value?.map((elm) => String(elm[value_key])) ?? [])
}, [value])
const onSelectChange = (rawValues?: SelectValue) => {
let values: any[] = []
if (typeof rawValues === 'string')
values = rawValues.split(',')
else if (Array.isArray(rawValues))
values = rawValues
const objectValues: T[] = values.reduce((out: T[], value: string) => {
const res = options.find((option) => String(option[value_key]) === String(value))
if (res) out.push(res)
return out
}, [])
onChange?.(objectValues)
}
return (
<Select
{...other}
mode={'tags'}
options={selectOptions}
value={selectedValue}
onChange={onSelectChange}
/>
)
})
export const makeTagColumn = <T extends Record<string, any>>(
title: string,
dataIndex: string,
options: T[],
value_key: string,
label_key: string,
other?: columnPropsOther,
tagOther?: SelectProps<SelectValue>
) => {
const InputComponent = makeTagInput<T>(value_key, label_key)
return makeColumn(title, dataIndex, {
...other,
render: (item?: T[]) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm) ?? elm[label_key]}</Tag>) ?? '-',
input: <InputComponent options={options} other={tagOther} />,
})
} }
type PaginationContainer = { type PaginationContainer = {
skip?: number skip?: number
take?: number take?: number
count?: number count?: number
items?: any[] | null items?: any[] | null
} }
export const makePaginationObject = (сontainer: PaginationContainer, ...other: any) => ({ export const makePaginationObject = (сontainer: PaginationContainer, ...other: any) => ({
...other, ...other,
pageSize: сontainer.take, pageSize: сontainer.take,
total: сontainer.count ?? сontainer.items?.length ?? 0, total: сontainer.count ?? сontainer.items?.length ?? 0,
current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1)) current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1))
}) })
export type TableContainer = TableProps<any> & {
dataSource: any[]
children?: ReactNode
}
export const Table = ({dataSource, children, ...other}: TableContainer) => (
<RawTable dataSource={tryAddKeys(dataSource)} {...other}>{children}</RawTable>
)

View File

@ -1,18 +1,25 @@
export const makeNumericSorter = (key: string) => (a: any, b: any) => Number(a[key]) - Number(b[key]) import { isRawDate } from '@utils'
export const makeStringSorter = (key: string) => (a: any, b: any) => { import { DataType } from './Columns'
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
if (!a && !b) return 0 if (!a && !b) return 0
if (!a) return 1 if (!a) return 1
if (!b) return -1 if (!b) return -1
return ('' + a[key]).localeCompare(b[key]) return String(a[key]).localeCompare(String(b[key]))
} }
export const makeDateSorter = (key: string) => (a: any, b: any) => { export const makeDateSorter = <T extends unknown>(key: keyof DataType<T>) => (a: DataType<T>, b: DataType<T>) => {
const date = new Date(a[key]) const adate = a[key]
const bdate = b[key]
if (Number.isNaN(date.getTime())) if (!isRawDate(adate) || !isRawDate(bdate))
throw new Error('Date column contains not date formatted string(s)') throw new Error('Date column contains not date formatted string(s)')
return date.getTime() - new Date(b[key]).getTime() const date = new Date(adate)
return date.getTime() - new Date(bdate).getTime()
} }

View File

@ -1,14 +1,14 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Upload, Button } from 'antd' import { Upload, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons' import { UploadOutlined } from '@ant-design/icons'
import { upload } from './factory' import { upload } from './factory'
import { ErrorFetch } from './ErrorFetch' import { ErrorFetch } from './ErrorFetch'
export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => { export const UploadForm = memo(({ url, disabled, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
const [fileList, setfileList] = useState([]) const [fileList, setfileList] = useState([])
const handleFileSend = async () => { const handleFileSend = useCallback(async () => {
onUploadStart?.() onUploadStart?.()
try { try {
const formDataLocal = new FormData() const formDataLocal = new FormData()
@ -27,12 +27,14 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
onUploadSuccess?.() onUploadSuccess?.()
} }
} catch(error) { } catch(error) {
if(process.env.NODE_ENV === 'development')
console.error(error)
onUploadError?.(error) onUploadError?.(error)
} finally { } finally {
setfileList([]) setfileList([])
onUploadComplete?.() onUploadComplete?.()
} }
} }, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
const isSendButtonEnabled = fileList.length > 0 const isSendButtonEnabled = fileList.length > 0
return( return(
@ -40,10 +42,11 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
<Upload <Upload
name={'file'} name={'file'}
accept={accept} accept={accept}
disabled={disabled}
fileList={fileList} fileList={fileList}
onChange={(props) => setfileList(props.fileList)} onChange={(props) => setfileList(props.fileList)}
> >
<Button icon={<UploadOutlined/>}>Загрузить файл</Button> <Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
</Upload> </Upload>
<Button <Button
type={'primary'} type={'primary'}
@ -56,3 +59,5 @@ export const UploadForm = memo(({ url, accept, style, formData, onUploadStart, o
</div> </div>
) )
}) })
export default UploadForm

View File

@ -1,5 +1,5 @@
import { memo, MouseEventHandler, useState } from 'react' import { memo, MouseEventHandler, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory, useLocation } from 'react-router-dom'
import { Button, Dropdown, DropDownProps, Menu } from 'antd' import { Button, Dropdown, DropDownProps, Menu } from 'antd'
import { UserOutlined } from '@ant-design/icons' import { UserOutlined } from '@ant-design/icons'
@ -14,16 +14,17 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false) const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
const history = useHistory() const history = useHistory()
const location = useLocation()
const onChangePasswordClick: MouseEventHandler = (e) => { const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
setIsModalVisible(true) setIsModalVisible(true)
e.preventDefault() e.preventDefault()
} }, [])
const onChangePasswordOk = () => { const onChangePasswordOk = useCallback(() => {
setIsModalVisible(false) setIsModalVisible(false)
history.push('/login') history.push({ pathname: '/login', state: { from: location.pathname }})
} }, [history, location])
return ( return (
<> <>
@ -33,15 +34,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
overlay={( overlay={(
<Menu style={{ textAlign: 'right' }}> <Menu style={{ textAlign: 'right' }}>
{isAdmin ? ( {isAdmin ? (
<PrivateMenuItemLink path={'/'} title={'Вернуться на сайт'}/> <PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/>
) : ( ) : (
<PrivateMenuItemLink path={'/admin'} title={'Панель администратора'}/> <PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/>
)} )}
<Menu.Item> <Menu.Item>
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link> <Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link>
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
<Link to={'/login'} onClick={removeUser}>Выход</Link> <Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
)} )}

View 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

View File

@ -1,7 +1,10 @@
import { TreeSelect } from 'antd' import { TreeSelect } from 'antd'
import { DefaultValueType } from 'rc-tree-select/lib/interface' import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode } from 'react' import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
import { useHistory, useRouteMatch } from 'react-router-dom' import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
import { LabelInValueType } from 'rc-select/lib/Select'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import LoaderPortal from './LoaderPortal' import LoaderPortal from './LoaderPortal'
@ -54,11 +57,12 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
return value return value
} }
export const WellTreeSelector = () => { export const WellTreeSelector = memo(({ ...other }) => {
const [wellsTree, setWellsTree] = useState<any[]>([]) // TODO: Исправить тип (необходимо разобраться с типом value rc-select) const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false) const [showLoader, setShowLoader] = useState<boolean>(false)
const [value, setValue] = useState<string>() const [value, setValue] = useState<string>()
const history = useHistory() const history = useHistory()
const location = useLocation()
const routeMatch = useRouteMatch('/:route/:id') const routeMatch = useRouteMatch('/:route/:id')
useEffect(() => { useEffect(() => {
@ -100,14 +104,15 @@ export const WellTreeSelector = () => {
setValue(getLabel(wellsTree, routeMatch?.url)) setValue(getLabel(wellsTree, routeMatch?.url))
}, [wellsTree, routeMatch]) }, [wellsTree, routeMatch])
const onChange = (value: string): void => { const onChange = useCallback((value: string): void => {
if (wellsTree) if (wellsTree)
setValue(getLabel(wellsTree, value)) setValue(getLabel(wellsTree, value))
} }, [wellsTree])
const onSelect = (value: string): void => { const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
if (value) history.push(value) if (['number', 'string'].includes(typeof value))
} history.push({ pathname: String(value), state: { from: location.pathname }})
}, [history, location])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -123,9 +128,10 @@ export const WellTreeSelector = () => {
onChange={onChange} onChange={onChange}
value={value} value={value}
style={{ width: '350px' }} style={{ width: '350px' }}
{...other}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default WellTreeSelector export default WellTreeSelector

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react' import { memo, useEffect, useRef, useState } from 'react'
import { import {
Chart, Chart,
TimeScale, TimeScale,
@ -178,7 +178,7 @@ export const timeParamsByInterval = (intervalSec: number): TimeParams => {
return { unit, stepSize } return { unit, stepSize }
} }
export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams}) => { export const ChartTimeBase = memo<ChartTimeBaseProps>(({ options, dataParams }) => {
const chartRef = useRef<HTMLCanvasElement>(null) const chartRef = useRef<HTMLCanvasElement>(null)
const [chart, setChart] = useState<any>() const [chart, setChart] = useState<any>()
@ -217,4 +217,4 @@ export const ChartTimeBase: React.FC<ChartTimeBaseProps> = ({options, dataParams
}, [chart, dataParams]) }, [chart, dataParams])
return(<canvas ref={chartRef} />) return(<canvas ref={chartRef} />)
} })

View File

@ -1,6 +1,7 @@
import { ChartOptions, Scriptable, ScriptableContext } from 'chart.js' import { ChartOptions, Scriptable, ScriptableContext } from 'chart.js'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { makeDateSorter } from '../Table'
import { makeDateSorter } from '@components/Table'
import { import {
ChartTimeBase, ChartTimeBase,
ChartTimeData, ChartTimeData,
@ -21,7 +22,8 @@ export type ColumnLineConfig = {
dash?: Array<number> dash?: Array<number>
borderColor?: string borderColor?: string
backgroundColor?: string backgroundColor?: string
borderWidth?: Scriptable<number, ScriptableContext<'radar'>> borderWidth?: Scriptable<number, ScriptableContext<'line'>>
showDatalabels?: boolean
fill?: string fill?: string
} }
export type ColumnPostParsing = (data: ChartTimeDataParams) => void export type ColumnPostParsing = (data: ChartTimeDataParams) => void
@ -44,7 +46,7 @@ const chartPluginsOptions: ChartOptions = {
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderRadius: 4, borderRadius: 4,
color: '#000B', color: '#000B',
display: context => (context.dataset.label === 'wellDepth') && 'auto', display: context => !!context.dataset.label?.endsWith(' ') && 'auto',
formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`, formatter: value => `${value.y.toLocaleTimeString()} ${value.label.toPrecision(4)}`,
padding: 6, padding: 6,
align: 'left', align: 'left',
@ -67,14 +69,14 @@ export const GetOrCreateDatasetByLineConfig = (data: ChartTimeData, lineConfig:
?? GetRandomColor() ?? GetRandomColor()
dataset = { dataset = {
label: lineConfig.label, label: lineConfig.label?.trimEnd() + (lineConfig.showDatalabels ? ' ' : ''),
data: [], data: [],
backgroundColor: lineConfig.backgroundColor ?? color, backgroundColor: lineConfig.backgroundColor ?? color,
borderColor: lineConfig.borderColor ?? color, borderColor: lineConfig.borderColor ?? color,
borderWidth: lineConfig.borderWidth ?? 1, borderWidth: lineConfig.borderWidth ?? 1,
borderDash: lineConfig.dash ?? [], borderDash: lineConfig.dash ?? [],
showLine: lineConfig.showLine ?? !lineConfig.isShape, showLine: lineConfig.showLine ?? !lineConfig.isShape,
fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none') fill: lineConfig.fill ?? (lineConfig.isShape ? 'shape' : 'none'),
} }
data.datasets.push(dataset) data.datasets.push(dataset)

View File

@ -36,7 +36,7 @@ type asyncFunction = (...args: any) => Promise<any|void>
export const invokeWebApiWrapperAsync = async ( export const invokeWebApiWrapperAsync = async (
funcAsync: asyncFunction, funcAsync: asyncFunction,
setShowLoader?: Dispatch<SetStateAction<boolean>>, setShowLoader?: Dispatch<SetStateAction<boolean>>,
errorNotifyText?: (string | ((ex: unknown) => string)), errorNotifyText?: ReactNode | ((ex: unknown) => ReactNode),
actionName?: string, actionName?: string,
) => { ) => {
setShowLoader?.(true) setShowLoader?.(true)

View File

@ -3,3 +3,4 @@ export type { WellIconColors, WellIconProps, WellIconState } from './WellIcon'
export { PointerIcon } from './PointerIcon' export { PointerIcon } from './PointerIcon'
export { WellIcon } from './WellIcon' export { WellIcon } from './WellIcon'
export { Loader } from './Loader'

View File

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

View File

@ -10,10 +10,12 @@ export type RoleViewProps = {
} }
export const RoleView = memo<RoleViewProps>(({ role }) => { export const RoleView = memo<RoleViewProps>(({ role }) => {
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )
const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0 const hasIncludedRoles = (role?.roles?.length && role.roles.length > 0) ? 1 : 0
const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0 const hasPermissions = (role?.permissions?.length && role.permissions.length > 0) ? 1 : 0
return role ? ( return (
<Tooltip <Tooltip
overlayInnerStyle={{ width: '400px' }} overlayInnerStyle={{ width: '400px' }}
title={ title={
@ -54,7 +56,5 @@ export const RoleView = memo<RoleViewProps>(({ role }) => {
> >
{role.caption} {role.caption}
</Tooltip> </Tooltip>
) : (
<Tooltip title={'нет данных'}>-</Tooltip>
) )
}) })

View File

@ -1,13 +1,11 @@
export type { PermissionViewProps } from './PermissionView' export type { PermissionViewProps } from './PermissionView'
export type { TelemetryViewProps } from './TelemetryView' export type { TelemetryViewProps } from './TelemetryView'
export type { CompanyViewProps } from './CompanyView' export type { CompanyViewProps } from './CompanyView'
export type { MarkViewProps } from './MarkView'
export type { RoleViewProps } from './RoleView' export type { RoleViewProps } from './RoleView'
export type { UserViewProps } from './UserView' export type { UserViewProps } from './UserView'
export { PermissionView } from './PermissionView' export { PermissionView } from './PermissionView'
export { TelemetryView, getTelemetryLabel } from './TelemetryView' export { TelemetryView, getTelemetryLabel } from './TelemetryView'
export { CompanyView } from './CompanyView' export { CompanyView } from './CompanyView'
export { MarkView } from './MarkView'
export { RoleView } from './RoleView' export { RoleView } from './RoleView'
export { UserView } from './UserView' export { UserView } from './UserView'

7
src/images/Logo.tsx Normal file
View 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

View File

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

View File

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import './styles/index.css'; import App from './App'
import App from './App'; import reportWebVitals from './reportWebVitals'
import reportWebVitals from './reportWebVitals';
ReactDOM.render( import '@styles/index.css'
ReactDOM.render((
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>
document.getElementById('root') ), document.getElementById('root'))
);
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(); reportWebVitals()

View File

@ -1,14 +1,17 @@
import { memo } from 'react' import { memo } from 'react'
import { Link } from 'react-router-dom'
export const AccessDenied = memo(() => ( export const AccessDenied = memo(() => (
<div style={{ <div style={{
width: '100vw', width: '100vw',
height: '100vh', height: '100vh',
display: 'flex', display: 'flex',
flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}}> }}>
Доступ запрещён <h2>Доступ запрещён</h2>
<Link to={'/login'}>На страницу входа</Link>
</div> </div>
)) ))

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { import {
EditableTable, EditableTable,
@ -6,7 +6,8 @@ import {
makeSelectColumn, makeSelectColumn,
makeActionHandler, makeActionHandler,
makeStringSorter, makeStringSorter,
defaultPagination defaultPagination,
makeTimezoneColumn
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -17,7 +18,7 @@ import { hasPermission } from '@utils/permissions'
import { coordsFixed } from './DepositController' import { coordsFixed } from './DepositController'
export const ClusterController = () => { export const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([]) const [clusters, setClusters] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -36,9 +37,10 @@ export const ClusterController = () => {
}), }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]
const updateTable = () => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
const clusters = await AdminClusterService.getAll() const clusters = await AdminClusterService.getAll()
setClusters(arrayOrDefault(clusters)) setClusters(arrayOrDefault(clusters))
@ -46,7 +48,7 @@ export const ClusterController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список кустов`, `Не удалось загрузить список кустов`,
'Получение списка кустов' 'Получение списка кустов'
) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -59,7 +61,7 @@ export const ClusterController = () => {
'Получение списка месторождений' 'Получение списка месторождений'
), []) ), [])
useEffect(updateTable, []) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = {
service: AdminClusterService, service: AdminClusterService,
@ -79,9 +81,10 @@ export const ClusterController = () => {
onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')} onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')}
onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')} onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')}
onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')} onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')}
tableName={'admin_cluster_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default ClusterController export default ClusterController

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { import {
EditableTable, EditableTable,
@ -16,15 +16,15 @@ import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
export const CompanyController = () => { export const CompanyController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [companies, setCompanies] = useState([]) const [companies, setCompanies] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const updateTable = async () => { const updateTable = useCallback(async () => {
const companies = await AdminCompanyService.getAll() const companies = await AdminCompanyService.getAll()
setCompanies(arrayOrDefault(companies)) setCompanies(arrayOrDefault(companies))
} }, [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async() => { async() => {
@ -51,7 +51,7 @@ export const CompanyController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список типов компаний`, `Не удалось загрузить список типов компаний`,
'Получение списка типов команд' 'Получение списка типов команд'
), []) ), [updateTable])
const handlerProps = { const handlerProps = {
service: AdminCompanyService, service: AdminCompanyService,
@ -76,9 +76,10 @@ export const CompanyController = () => {
onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')} onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')}
onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')} onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')}
onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')} onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')}
tableName={'admin_company_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default CompanyController export default CompanyController

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { import {
EditableTable, EditableTable,
@ -12,7 +12,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api' import { AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@asb/utils/permissions' import { hasPermission } from '@utils/permissions'
const columns = [ const columns = [
makeColumn('Название', 'caption', { makeColumn('Название', 'caption', {
@ -23,11 +23,11 @@ const columns = [
}), }),
] ]
export const CompanyTypeController = () => { export const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([]) const [companyTypes, setCompanyTypes] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const updateTable = () => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async() => { async() => {
const companyTypes = await AdminCompanyTypeService.getAll() const companyTypes = await AdminCompanyTypeService.getAll()
setCompanyTypes(arrayOrDefault(companyTypes)) setCompanyTypes(arrayOrDefault(companyTypes))
@ -35,9 +35,9 @@ export const CompanyTypeController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список типов компаний`, `Не удалось загрузить список типов компаний`,
'Получение списка типов компаний' 'Получение списка типов компаний'
) ), [])
useEffect(updateTable, []) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = {
service: AdminCompanyTypeService, service: AdminCompanyTypeService,
@ -57,9 +57,10 @@ export const CompanyTypeController = () => {
onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')} onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')}
onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')} onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')}
onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')} onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')}
tableName={'admin_company_type_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default CompanyTypeController export default CompanyTypeController

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, makeActionHandler, defaultPagination } from '@components/Table' import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table'
import { AdminDepositService } from '@api' import { AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
@ -13,14 +13,15 @@ export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).to
const depositColumns = [ const depositColumns = [
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }) makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]
export const DepositController = () => { export const DepositController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const updateTable = () => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async() => { async() => {
const deposits = await AdminDepositService.getAll() const deposits = await AdminDepositService.getAll()
setDeposits(arrayOrDefault(deposits)) setDeposits(arrayOrDefault(deposits))
@ -28,9 +29,9 @@ export const DepositController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список месторождении`, `Не удалось загрузить список месторождении`,
'Получение списка месторождений' 'Получение списка месторождений'
) ), [])
useEffect(updateTable, []) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = {
service: AdminDepositService, service: AdminDepositService,
@ -50,9 +51,10 @@ export const DepositController = () => {
onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')} onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')}
onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')} onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')}
onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')} onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')}
tableName={'admin_deposit_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default DepositController export default DepositController

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { import {
EditableTable, EditableTable,
@ -26,11 +26,11 @@ const columns = [
}), }),
] ]
export const PermissionController = () => { export const PermissionController = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const updateTable = async () => invokeWebApiWrapperAsync( const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
async () => { async () => {
const permission = await AdminPermissionService.getAll() const permission = await AdminPermissionService.getAll()
setPermissions(arrayOrDefault(permission)) setPermissions(arrayOrDefault(permission))
@ -38,9 +38,9 @@ export const PermissionController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список прав`, `Не удалось загрузить список прав`,
'Получение списка прав' 'Получение списка прав'
) ), [])
useEffect(() => updateTable(), []) useEffect(() => updateTable(), [updateTable])
const handlerProps = { const handlerProps = {
service: AdminPermissionService, service: AdminPermissionService,
@ -60,9 +60,10 @@ export const PermissionController = () => {
onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')} onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')}
onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')} onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')}
onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')} onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')}
tableName={'admin_permission_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default PermissionController export default PermissionController

View File

@ -1,4 +1,4 @@
import { memo, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { PermissionView, RoleView } from '@components/views' import { PermissionView, RoleView } from '@components/views'
@ -15,20 +15,21 @@ export const RoleController = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const loadRoles = async () => { const loadRoles = useCallback(async () => {
const roles = await AdminUserRoleService.getAll() const roles = await AdminUserRoleService.getAll()
setRoles(arrayOrDefault(roles)) setRoles(arrayOrDefault(roles))
} }, [])
useEffect(() => { useEffect(() => {
setColumns([ setColumns([
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), makeColumn('Название', 'caption', { width: 100, editable: true, formItemRules: min1 }),
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
width: 200, width: 400,
editable: true, editable: true,
render: (role) => <RoleView role={role} /> render: (role) => <RoleView role={role} />
}, { allowClear: true }), }, { allowClear: true }),
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
width: 600,
editable: true, editable: true,
render: (permission) => <PermissionView info={permission} />, render: (permission) => <PermissionView info={permission} />,
}), }),
@ -44,7 +45,7 @@ export const RoleController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список ролей`, `Не удалось загрузить список ролей`,
'Получение списка ролей' 'Получение списка ролей'
), []) ), [loadRoles])
const handlerProps = { const handlerProps = {
service: AdminUserRoleService, service: AdminUserRoleService,
@ -68,6 +69,7 @@ export const RoleController = memo(() => {
onRowAdd={hasPermission('AdminUserRole.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление роли')} onRowAdd={hasPermission('AdminUserRole.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление роли')}
onRowEdit={hasPermission('AdminUserRole.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование роли')} onRowEdit={hasPermission('AdminUserRole.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование роли')}
onRowDelete={hasPermission('AdminUserRole.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление роли')} onRowDelete={hasPermission('AdminUserRole.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление роли')}
tableName={'admin_role_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { import {
defaultPagination, defaultPagination,
@ -29,7 +29,7 @@ const columns = [
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'), makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
] ]
export const TelemetryController = () => { export const TelemetryController = memo(() => {
const [telemetryData, setTelemetryData] = useState([]) const [telemetryData, setTelemetryData] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -56,9 +56,10 @@ export const TelemetryController = () => {
columns={columns} columns={columns}
dataSource={telemetryData} dataSource={telemetryData}
pagination={defaultPagination} pagination={defaultPagination}
tableName={'admin_telemetry_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default TelemetryController export default TelemetryController

View File

@ -1,6 +1,7 @@
import { Button, Tag } from 'antd' import { Button, Input, Tag } from 'antd'
import { UserSwitchOutlined } from '@ant-design/icons' import { UserSwitchOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { import {
EditableTable, EditableTable,
@ -17,29 +18,71 @@ import { ChangePassword } from '@components/ChangePassword'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api' import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules' import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
import { makeTextOnFilter, makeTextFilters } from '@utils/table' import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/table'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import RoleTag from './RoleTag' import RoleTag from './RoleTag'
const SEARCH_TIMEOUT = 400
export const UserController = () => { export const UserController = memo(() => {
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [filteredUsers, setFilteredUsers] = useState([])
const [searchValue, setSearchValue] = useState('')
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [isSearching, setIsSearching] = useState(false)
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [selectedUser, setSelectedUser] = useState(null) const [selectedUser, setSelectedUser] = useState(null)
const [subject, setSubject] = useState(null)
const additionalButtons = (record, editingKey) => ( useEffect(() => invokeWebApiWrapperAsync(
async () => {
const filteredUsers = users.filter((user) => user && [
user.login ?? '',
user.name ?? '',
user.surname ?? '',
user.partonymic ?? '',
user.email ?? '',
user.phone ?? '',
user.position ?? '',
user.company?.caption ?? '',
].join(' ').toLowerCase().includes(searchValue))
setFilteredUsers(filteredUsers)
},
setIsSearching,
`Не удалось произвести поиск пользователей`
), [users, searchValue])
useEffect(() => {
if (!subject) {
const sub = new BehaviorSubject('')
setSubject(sub)
} else {
const observable = subject.pipe(
debounceTime(SEARCH_TIMEOUT),
map(s => s.trim().toLowerCase() ?? ''),
distinctUntilChanged(),
filter(s => s.length <= 0 || s.length >= 2),
).subscribe(value => setSearchValue(value))
return () => {
observable.unsubscribe()
subject.unsubscribe()
}
}
}, [subject])
const additionalButtons = useCallback((record, editingKey) => (
<Button <Button
icon={<UserSwitchOutlined />} icon={<UserSwitchOutlined />}
onClick={() => setSelectedUser(record)} onClick={() => setSelectedUser(record)}
title={'Сменить пароль'} title={'Сменить пароль'}
disabled={editingKey !== ''} disabled={editingKey !== ''}
/> />
) ), [])
const updateTable = () => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async() => { async() => {
const users = await AdminUserService.getAll() const users = await AdminUserService.getAll()
setUsers(arrayOrDefault(users)) setUsers(arrayOrDefault(users))
@ -47,7 +90,7 @@ export const UserController = () => {
setShowLoader, setShowLoader,
`Не удалось загрузить список пользователей`, `Не удалось загрузить список пользователей`,
'Получение списка пользователей' 'Получение списка пользователей'
) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -61,7 +104,7 @@ export const UserController = () => {
setUsers(users) setUsers(users)
const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email']) const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email'])
const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))]
setColumns([ setColumns([
makeColumn('Логин', 'login', { makeColumn('Логин', 'login', {
@ -124,6 +167,8 @@ export const UserController = () => {
makeColumn('Роли', 'roleNames', { makeColumn('Роли', 'roleNames', {
editable: true, editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
filters: roleFilters,
onFilter: makeArrayOnFilter('roleNames'),
render: (item) => item?.map((elm) => ( render: (item) => item?.map((elm) => (
<Tag key={elm} color={'blue'}> <Tag key={elm} color={'blue'}>
<RoleView role={roles.find((role) => role.caption === elm)} /> <RoleView role={roles.find((role) => role.caption === elm)} />
@ -148,20 +193,30 @@ export const UserController = () => {
onComplete: updateTable, onComplete: updateTable,
} }
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
return ( return (
<> <>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Input.Search
allowClear
placeholder={'Поиск пользователей'}
onChange={onSearchTextChange}
style={{ marginBottom: '15px' }}
loading={isSearching}
/>
<EditableTable <EditableTable
size={'small'} size={'small'}
bordered bordered
columns={columns} columns={columns}
dataSource={users} dataSource={filteredUsers}
onRowAdd={hasPermission('AdminUser.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление пользователя')} onRowAdd={hasPermission('AdminUser.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление пользователя')}
onRowEdit={hasPermission('AdminUser.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование пользователя')} onRowEdit={hasPermission('AdminUser.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование пользователя')}
onRowDelete={hasPermission('AdminUser.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление пользователя')} onRowDelete={hasPermission('AdminUser.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление пользователя')}
additionalButtons={additionalButtons} additionalButtons={additionalButtons}
buttonsWidth={120} buttonsWidth={120}
pagination={defaultPagination} pagination={defaultPagination}
tableName={'admin_user_controller'}
/> />
</LoaderPortal> </LoaderPortal>
<ChangePassword <ChangePassword
@ -172,6 +227,6 @@ export const UserController = () => {
/> />
</> </>
) )
} })
export default UserController export default UserController

View File

@ -41,6 +41,7 @@ export const VisitLog = memo(() => {
columns={columns} columns={columns}
dataSource={logData} dataSource={logData}
pagination={defaultPagination} pagination={defaultPagination}
tableName={'visit_log'}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Button } from 'antd' import { Button } from 'antd'
import { CopyOutlined } from '@ant-design/icons' import { CopyOutlined } from '@ant-design/icons'
import { memo, useEffect, useState } from 'react'
import { import {
AdminClusterService, AdminClusterService,
@ -17,24 +17,30 @@ import {
makeNumericSorter, makeNumericSorter,
makeTagColumn, makeTagColumn,
defaultPagination, defaultPagination,
makeTimezoneColumn,
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views' import { TelemetryView, CompanyView } from '@components/views'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { coordsFixed } from '../DepositController' import { coordsFixed } from '../DepositController'
import TelemetrySelect from './TelemetrySelect' import TelemetrySelect from './TelemetrySelect'
import '@styles/admin.css' import '@styles/admin.css'
import { hasPermission } from '@asb/utils/permissions'
const wellTypes = [ const wellTypes = [
{ value: 1, label: 'Наклонно-направленная' }, { value: 1, label: 'Наклонно-направленная' },
{ value: 2, label: 'Горизонтальная' }, { value: 2, label: 'Горизонтальная' },
] ]
export const WellController = () => { const recordParser = (record) => ({
...record,
idTelemetry: record.telemetry?.id,
})
export const WellController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [wells, setWells] = useState([]) const [wells, setWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -53,14 +59,14 @@ export const WellController = () => {
// TODO: Метод дубликации скважины // TODO: Метод дубликации скважины
} }
const addititonalButtons = (record, editingKey) => ( const addititonalButtons = memo((record, editingKey) => (
<Button <Button
icon={<CopyOutlined />} icon={<CopyOutlined />}
title={'Дублировать скважину'} title={'Дублировать скважину'}
disabled={(editingKey ?? '') !== ''} disabled={(editingKey ?? '') !== ''}
onClick={() => duplicateWell(record)} onClick={() => duplicateWell(record)}
/> />
) ))
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -94,6 +100,7 @@ export const WellController = () => {
render: (telemetry) => <TelemetryView telemetry={telemetry} />, render: (telemetry) => <TelemetryView telemetry={telemetry} />,
input: <TelemetrySelect telemetry={telemetry}/>, input: <TelemetrySelect telemetry={telemetry}/>,
}, ), }, ),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
editable: true, editable: true,
render: (company) => <CompanyView company={company} />, render: (company) => <CompanyView company={company} />,
@ -107,11 +114,6 @@ export const WellController = () => {
'Получение списка кустов' 'Получение списка кустов'
), []) ), [])
const recordParser = (record) => ({
...record,
idTelemetry: record.telemetry?.id,
})
const handlerProps = { const handlerProps = {
service: AdminWellService, service: AdminWellService,
setLoader: setShowLoader, setLoader: setShowLoader,
@ -132,9 +134,10 @@ export const WellController = () => {
onRowDelete={hasPermission('AdminWell.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление скважины')} onRowDelete={hasPermission('AdminWell.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление скважины')}
//additionalButtons={addititonalButtons} //additionalButtons={addititonalButtons}
buttonsWidth={95} buttonsWidth={95}
tableName={'admin_well_controller'}
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default WellController export default WellController

View 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

View File

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd' import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd'
@ -9,15 +9,15 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table' import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
import { DrillParamsService, WellCompositeService } from '@api' import { DrillParamsService, WellCompositeService } from '@api'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
import { import {
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction, makeFilterMinMaxFunction,
getOperations getOperations
} from '@pages/Cluster/functions' } from '@utils/functions'
import { Tvd } from '@pages/WellOperations/Tvd'
import { getColumns } from '@pages/WellOperations/WellDrillParams'
import WellOperationsTable from '@pages/Cluster/WellOperationsTable' import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
import { Tvd } from '../Tvd'
import { getColumns } from '../WellDrillParams'
const filtersMinMax = [ const filtersMinMax = [
@ -42,6 +42,8 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false) const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const location = useLocation()
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell]) useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
useEffect(() => { useEffect(() => {
@ -125,7 +127,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const columns = [ const columns = [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(text, item) => <Link to={`/well/${item?.id}`}>{text ?? '-'}</Link> (text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
), ),
makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'), makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'),
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
@ -187,7 +189,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
) )
} }
const onParamButtonClick = () => invokeWebApiWrapperAsync( const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
setIsParamsModalVisible(true) setIsParamsModalVisible(true)
const params = await DrillParamsService.getCompositeAll(idWell) const params = await DrillParamsService.getCompositeAll(idWell)
@ -196,9 +198,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
setShowParamsLoader, setShowParamsLoader,
`Не удалось загрузить список режимов для скважины "${idWell}"`, `Не удалось загрузить список режимов для скважины "${idWell}"`,
'Получение списка режимов скважины' 'Получение списка режимов скважины'
) ), [idWell])
const onParamsAddClick = () => invokeWebApiWrapperAsync( const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
await DrillParamsService.save(idWell, params) await DrillParamsService.save(idWell, params)
setIsParamsModalVisible(false) setIsParamsModalVisible(false)
@ -206,7 +208,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
setShowLoader, setShowLoader,
`Не удалось добавить режимы в список скважины "${idWell}"`, `Не удалось добавить режимы в список скважины "${idWell}"`,
'Добавление режима скважины' 'Добавление режима скважины'
) ), [idWell, params])
return ( return (
<> <>

View File

@ -1,15 +1,14 @@
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { Switch, useParams } from 'react-router-dom' import { Switch, useParams } from 'react-router-dom'
import { Col, Layout, Menu, Row, Tag, TreeSelect } from 'antd' import { Col, Layout, Menu, Row } from 'antd'
import { import {
DepositService,
OperationStatService, OperationStatService,
WellCompositeService, WellCompositeService,
} from '@api' } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { hasPermission } from '@utils/permissions'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import WellSelector from '@components/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private' import { PrivateDefaultRoute, PrivateMenuItemLink, PrivateRoute } from '@components/Private'
@ -18,57 +17,28 @@ import { WellCompositeSections } from './WellCompositeSections'
const { Content } = Layout const { Content } = Layout
export const WellCompositeEditor = memo(({ idWell }) => { export const WellCompositeEditor = memo(({ idWell, rootPath }) => {
const rootPath = `/well/${idWell}/operations/composite`
const { tab } = useParams() const { tab } = useParams()
const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([])
const [statsWells, setStatsWells] = useState([]) const [statsWells, setStatsWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [showTabLoader, setShowTabLoader] = useState(false) const [showTabLoader, setShowTabLoader] = useState(false)
const [selectedIdWells, setSelectedIdWells] = useState([]) const [selectedIdWells, setSelectedIdWells] = useState([])
const [selectedSections, setSelectedSections] = useState([]) const [selectedSections, setSelectedSections] = useState([])
useEffect(() => { useEffect(() => invokeWebApiWrapperAsync(
invokeWebApiWrapperAsync( async () => {
async () => { try {
const deposits = await DepositService.getDeposits() const selected = await WellCompositeService.get(idWell)
const labels = {} setSelectedSections(arrayOrDefault(selected))
const wellsTree = deposits.map((deposit, dIdx) => ({ } catch(e) {
title: deposit.caption, setSelectedSections([])
key: `0-${dIdx}`, }
value: `0-${dIdx}`, },
children: deposit.clusters.map((cluster, cIdx) => ({ setShowLoader,
title: cluster.caption, 'Не удалось загрузить список скважин',
key: `0-${dIdx}-${cIdx}`, 'Получение списка скважин'
value: `0-${dIdx}-${cIdx}`, ), [idWell])
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))
} catch(e) {
setSelectedSections([])
}
},
setShowLoader,
'Не удалось загрузить список скважин',
'Получение списка скважин'
)
}, [idWell])
useEffect(() => { useEffect(() => {
const wellIds = selectedSections.map((value) => value.idWellSrc) const wellIds = selectedSections.map((value) => value.idWellSrc)
@ -87,24 +57,12 @@ export const WellCompositeEditor = memo(({ idWell }) => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Row align={'middle'} justify={'space-between'} wrap={false}> <Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
<Col span={18}> <Col span={18}>
<TreeSelect <WellSelector
multiple idWell={idWell}
treeCheckable onChange={setSelectedIdWells}
showCheckedStrategy={TreeSelect.SHOW_CHILD}
treeDefaultExpandAll
treeData={wellsTree}
treeLine={{ showLeafIcon: false }}
onChange={(value) => setSelectedIdWells(value)}
size={'middle'}
style={{ width: '100%' }}
value={selectedIdWells} value={selectedIdWells}
placeholder={'Выберите скважины'}
tagRender={(props) => (
<Tag {...props}>{wellLabels[props.value] ?? props.label}</Tag>
)}
disabled={!hasPermission('WellOperation.edit')}
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>

View 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

View File

@ -1,5 +1,5 @@
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect } from 'react' import { useState, useEffect, memo } from 'react'
import { Tag, Button, Modal } from 'antd' import { Tag, Button, Modal } from 'antd'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
@ -16,13 +16,14 @@ import { CompanyView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import PointerIcon from '@components/icons/PointerIcon' import PointerIcon from '@components/icons/PointerIcon'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Tvd } from '@pages/WellOperations/Tvd'
import { import {
getOperations, getOperations,
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction makeFilterMinMaxFunction
} from './functions' } from '@utils/functions'
import { isRawDate } from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd'
import WellOperationsTable from './WellOperationsTable' import WellOperationsTable from './WellOperationsTable'
const filtersMinMax = [ const filtersMinMax = [
@ -34,7 +35,9 @@ const filtersWellsType = []
const DAY_IN_MS = 86_400_000 const DAY_IN_MS = 86_400_000
const ONLINE_DEADTIME = 600_000 const ONLINE_DEADTIME = 600_000
export const ClusterWells = ({ statsWells }) => { const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
export const ClusterWells = memo(({ statsWells }) => {
const [selectedWellId, setSelectedWellId] = useState(0) const [selectedWellId, setSelectedWellId] = useState(0)
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
@ -42,6 +45,8 @@ export const ClusterWells = ({ statsWells }) => {
const [tableData, setTableData] = useState([]) const [tableData, setTableData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const location = useLocation()
useEffect(() => { useEffect(() => {
if (!isOpsModalVisible || selectedWellId <= 0) { if (!isOpsModalVisible || selectedWellId <= 0) {
setWellOperations([]) setWellOperations([])
@ -61,14 +66,11 @@ export const ClusterWells = ({ statsWells }) => {
useEffect(() => { useEffect(() => {
let data = statsWells?.map((well) => { let data = statsWells?.map((well) => {
if (!filtersWellsType.some((el) => el.text === well.wellType)) if (!filtersWellsType.some((el) => el.text === well.wellType))
filtersWellsType.push({ text: well.wellType, value: well.wellType,}) filtersWellsType.push({ text: well.wellType, value: well.wellType })
let periodPlanValue = well.total?.plan?.start && well.total?.plan?.end const dateOrM = (a, b) => a && b ? (new Date(b) - new Date(a)) / DAY_IN_MS : '-'
? (new Date(well.total?.plan?.end) - new Date(well.total?.plan?.start)) / DAY_IN_MS const periodPlanValue = dateOrM(well.total?.plan?.start, well.total?.plan?.end)
: '-' const periodFactValue = dateOrM(well.total?.fact?.start, well.total?.fact?.end)
let periodFactValue = well.total?.fact?.start && well.total?.fact?.end
? (new Date(well.total?.fact?.end) - new Date(well.total?.fact?.start)) / DAY_IN_MS
: '-'
return { return {
key: well.caption, key: well.caption,
@ -106,14 +108,10 @@ export const ClusterWells = ({ statsWells }) => {
setTableData(data) setTableData(data)
}, [statsWells]) }, [statsWells])
const getDate = (str) => Number.isNaN(+new Date(str)) || +new Date(str) === 0
? '-'
: new Date(str).toLocaleString()
const columns = [ const columns = [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(_, item) => ( (_, item) => (
<Link to={`/well/${item.id}`} style={{display: 'flex', alignItems: 'center'}}> <Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}>
<PointerIcon <PointerIcon
state={item.idState === 1 ? 'active' : 'unknown'} state={item.idState === 1 ? 'active' : 'unknown'}
width={32} width={32}
@ -172,6 +170,7 @@ export const ClusterWells = ({ statsWells }) => {
bordered bordered
pagination={false} pagination={false}
rowKey={(record) => record.caption} rowKey={(record) => record.caption}
tableName={'cluster'}
/> />
<Modal <Modal
@ -199,6 +198,6 @@ export const ClusterWells = ({ statsWells }) => {
</Modal> </Modal>
</> </>
) )
} })
export default ClusterWells export default ClusterWells

View File

@ -1,9 +1,7 @@
import { Table } from 'antd' import { Table } from 'antd'
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table' import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
import { getPrecision } from '@utils/functions'
import { getPrecision } from './functions'
export const WellOperationsTable = ({ wellOperations }) => { export const WellOperationsTable = ({ wellOperations }) => {
const columns = [ const columns = [
@ -27,7 +25,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
commentFact: el.fact?.comment ?? '-' commentFact: el.fact?.comment ?? '-'
})) }))
return( return (
<Table <Table
bordered bordered
size={'small'} size={'small'}
@ -35,6 +33,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
dataSource={operations} dataSource={operations}
rowKey={(record) => record.key} rowKey={(record) => record.key}
pagination={{ defaultPageSize: 10 }} pagination={{ defaultPageSize: 10 }}
tableName={'well_operations'}
/> />
) )
} }

View File

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

View File

@ -1,5 +1,5 @@
import { Map, Overlay } from 'pigeon-maps' import { Map, Overlay } from 'pigeon-maps'
import { Link } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { ClusterService } from '@api' import { ClusterService } from '@api'
@ -8,6 +8,8 @@ import { PointerIcon } from '@components/icons'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import '@styles/index.css'
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 } const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
const calcViewParams = (clusters) => { const calcViewParams = (clusters) => {
@ -41,6 +43,8 @@ export const Deposit = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams) const [viewParams, setViewParams] = useState(defaultViewParams)
const location = useLocation()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
const data = await ClusterService.getClusters() const data = await ClusterService.getClusters()
@ -62,7 +66,7 @@ export const Deposit = memo(() => {
anchor={[cluster.latitude, cluster.longitude]} anchor={[cluster.latitude, cluster.longitude]}
key={`${cluster.latitude} ${cluster.longitude}`} key={`${cluster.latitude} ${cluster.longitude}`}
> >
<Link to={`/cluster/${cluster.id}`}> <Link to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
<PointerIcon state={'active'} width={48} height={59} /> <PointerIcon state={'active'} width={48} height={59} />
<span>{cluster.caption}</span> <span>{cluster.caption}</span>
</Link> </Link>

View File

@ -1,4 +1,3 @@
import moment from 'moment'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { DatePicker, Button, Input } from 'antd' import { DatePicker, Button, Input } from 'antd'
@ -9,6 +8,7 @@ import { UploadForm } from '@components/UploadForm'
import { CompanyView, UserView } from '@components/views' import { CompanyView, UserView } from '@components/views'
import { EditableTable, makePaginationObject } from '@components/Table' import { EditableTable, makePaginationObject } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
import { formatDate } from '@utils'
const pageSize = 12 const pageSize = 12
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
@ -28,7 +28,7 @@ const columns = [
title: 'Дата загрузки', title: 'Дата загрузки',
key: 'uploadDate', key: 'uploadDate',
dataIndex: 'uploadDate', dataIndex: 'uploadDate',
render: item => moment.utc(item).local().format('DD MMM YYYY, HH:mm:ss'), render: item => formatDate(item, false, 'DD MMM YYYY, HH:mm:ss'),
}, { }, {
title: 'Размер', title: 'Размер',
key: 'size', key: 'size',
@ -46,7 +46,7 @@ const columns = [
} }
] ]
export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange}) => { export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange, tableName }) => {
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [filterDataRange, setFilterDataRange] = useState([]) const [filterDataRange, setFilterDataRange] = useState([])
const [filterCompanyName, setFilterCompanyName] = useState([]) const [filterCompanyName, setFilterCompanyName] = useState([])
@ -173,6 +173,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
}} }}
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete} onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={tableName ?? `file_${idCategory}`}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -46,7 +46,7 @@ export const MenuDocuments = memo(({ idWell }) => {
<Switch> <Switch>
{documentCategories.map(category => ( {documentCategories.map(category => (
<PrivateRoute path={join(root, category.key)} key={`${category.key}`}> <PrivateRoute path={join(root, category.key)} key={`${category.key}`}>
<DocumentsTemplate idCategory={category.id} idWell={idWell} /> <DocumentsTemplate idCategory={category.id} idWell={idWell} tableName={`documents_${category.key}`} />
</PrivateRoute> </PrivateRoute>
))} ))}
<PrivateDefaultRoute urls={documentCategories.map((cat) => join(root, cat.key))}/> <PrivateDefaultRoute urls={documentCategories.map((cat) => join(root, cat.key))}/>

View File

@ -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>
&nbsp;'{lastUpdatedFile.name}'
&nbsp;[{formatBytes(lastUpdatedFile.size)}]
&nbsp;загружен: {new Date(lastUpdatedFile.uploadDate).toLocaleString()}
&nbsp;автор: <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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -1,55 +1,58 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory, useLocation } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons' import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { AuthService } from '@api'
import { setUser } from '@utils/storage'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { loginRules, passwordRules } from '@utils/validationRules' import { loginRules, passwordRules } from '@utils/validationRules'
import { setUser } from '@utils/storage'
import { AuthService } from '@api'
import '@styles/index.css' import '@styles/index.css'
import logo from '@images/logo_32.png' import Logo from '@images/Logo'
const logoIcon = <img src={logo} alt={'АСБ'} className={'logo'} width={130} />
export const Login = memo(() => { export const Login = memo(() => {
const history = useHistory() const history = useHistory()
const location = useLocation()
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const handleLogin = (formData) => invokeWebApiWrapperAsync( const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
const user = await AuthService.login(formData) const user = await AuthService.login(formData)
if (!user) throw Error('Неправильный логин или пароль') if (!user) throw Error('Неправильный логин или пароль')
setUser(user) setUser(user)
history.push('well') console.log(location.state?.from)
history.push(location.state?.from ?? 'well')
}, },
setShowLoader, setShowLoader,
(ex) => ex?.message ?? 'Ошибка входа', (ex) => ex?.message ?? 'Ошибка входа',
'Вход в систему' 'Вход в систему'
) ), [history, location])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }} extra={logoIcon}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Form onFinish={handleLogin}> <Logo style={{ marginBottom: '10px' }}/>
<Form.Item name={'login'} rules={loginRules}> <Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}>
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} /> <Form onFinish={handleLogin}>
</Form.Item> <Form.Item name={'login'} rules={loginRules}>
<Form.Item name={'password'} rules={passwordRules}> <Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
<Input.Password placeholder={'Пароль'} prefix={<LockOutlined />} /> </Form.Item>
</Form.Item> <Form.Item name={'password'} rules={passwordRules}>
<Form.Item> <Input.Password placeholder={'Пароль'} prefix={<LockOutlined />} />
<div className={'login-button'}> </Form.Item>
<Button type={'primary'} htmlType={'submit'}>Вход</Button> <Form.Item>
<div className={'login-button'}>
<Button type={'primary'} htmlType={'submit'}>Вход</Button>
</div>
</Form.Item>
<div className={'text-align-center'}>
<Link to={`/register`}>Отправить заявку на регистрацию</Link>
</div> </div>
</Form.Item> </Form>
<div className={'text-align-center'}> </Card>
<Link to={`/register`}>Отправить заявку на регистрацию</Link> </div>
</div>
</Form>
</Card>
</LoaderPortal> </LoaderPortal>
) )
}) })

View File

@ -48,6 +48,7 @@ export const InclinometryTable = memo(({ group, visible, onClose }) => {
columns={tableColumns} columns={tableColumns}
scroll={tableScroll} scroll={tableScroll}
bordered bordered
tableName={'measure_inclinometry'}
/> />
</Modal> </Modal>
) )

View File

@ -11,14 +11,14 @@ import {
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { MeasureService } from '@api' import { hasPermission } from '@utils/permissions'
import { formatDate } from '@utils' import { formatDate } from '@utils'
import { MeasureService } from '@api'
import { View } from './View' import { View } from './View'
import '@styles/index.css' import '@styles/index.css'
import '@styles/measure.css' import '@styles/measure.css'
import { hasPermission } from '@asb/utils/permissions'
const createEditingColumns = (cols, renderDelegate) => const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => ({ render: renderDelegate, ...col })) cols.map(col => ({ render: renderDelegate, ...col }))
@ -138,20 +138,20 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona
<div className={'measure-dates mt-20px'}> <div className={'measure-dates mt-20px'}>
<Timeline className={'mt-12px ml-10px'}> <Timeline className={'mt-12px ml-10px'}>
{data.map((item, index) => {data.map((item, index) => (
<Timeline.Item <Timeline.Item
key={index} key={index}
className={'measure-button'} className={'measure-button'}
onClick={() => setDisplayedValues(item)} onClick={() => setDisplayedValues(item)}
dot={item?.id === displayedValues?.id && dot={item?.id === displayedValues?.id && (
<CheckSquareOutlined className={'timeline-clock-icon'} /> <CheckSquareOutlined className={'timeline-clock-icon'} />
} )}
> >
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}> <span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}>
{formatDate(item.timestamp, true) ?? 'Нет данных'} {formatDate(item.timestamp) ?? 'Нет данных'}
</span> </span>
</Timeline.Item> </Timeline.Item>
)} ))}
</Timeline> </Timeline>
</div> </div>
</div> </div>

View File

@ -136,6 +136,7 @@ export const Messages = memo(({ idWell }) => {
onChange: (page) => setPage(page) onChange: (page) => setPage(page)
}} }}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={'messages'}
/> />
</LoaderPortal> </LoaderPortal>
</> </>

View File

@ -1,4 +1,4 @@
import { memo, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useHistory } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { import {
@ -21,7 +21,7 @@ import {
phoneRules phoneRules
} from '@utils/validationRules' } from '@utils/validationRules'
import logo from '@images/logo_32.png' import Logo from '@images/Logo'
const surnameRules = [...nameRules, { required: true, message: 'Пожалуйста, введите фамилию!' }] const surnameRules = [...nameRules, { required: true, message: 'Пожалуйста, введите фамилию!' }]
const regEmailRules = [{ required: true, message: 'Пожалуйста, введите email!' }, ...emailRules] const regEmailRules = [{ required: true, message: 'Пожалуйста, введите email!' }, ...emailRules]
@ -37,7 +37,7 @@ const confirmPasswordRules = [
}) })
] ]
const logoIcon = <img src={logo} alt={'АСБ'} className={'logo'} width={130}/> const logoIcon = <Logo width={130} />
const showPasswordIcon = visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />) const showPasswordIcon = visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)
const createInput = (name, placeholder, rules, isPassword, dependencies) => ( const createInput = (name, placeholder, rules, isPassword, dependencies) => (
@ -54,7 +54,7 @@ export const Register = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const history = useHistory()
const handleRegister = (formData) => invokeWebApiWrapperAsync( const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
await AuthService.register(formData) await AuthService.register(formData)
history.push('/login') history.push('/login')
@ -62,7 +62,7 @@ export const Register = memo(() => {
setShowLoader, setShowLoader,
`Ошибка отправки заявки на регистрацию`, `Ошибка отправки заявки на регистрацию`,
'Отправка заявки на регистрацию' 'Отправка заявки на регистрацию'
) ), [history])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>

View File

@ -78,6 +78,7 @@ export const Reports = memo(({ idWell }) => {
columns={columns} columns={columns}
dataSource={reports} dataSource={reports}
pagination={{ pageSize: 13 }} pagination={{ pageSize: 13 }}
tableName={'reports'}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -1,12 +1,13 @@
import 'moment/locale/ru' import 'moment/locale/ru'
import moment from 'moment' import moment from 'moment'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { DatePicker, Radio, Button, Select, notification } from 'antd' import { Radio, Button, Select, notification } from 'antd'
import { ReportService } from '@api' import { ReportService } from '@api'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
import LoaderPortal from '@components/LoaderPortal' import { LoaderPortal } from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DateRangeWrapper } from 'components/Table/DateRangeWrapper'
import { Reports } from './Reports' import { Reports } from './Reports'
import { ReportCreationNotify } from './ReportCreationNotify' import { ReportCreationNotify } from './ReportCreationNotify'
@ -133,11 +134,9 @@ export const Report = memo(({ idWell }) => {
<div className={'w-100 mt-20px mb-20px d-flex'}> <div className={'w-100 mt-20px mb-20px d-flex'}>
<div> <div>
<div>Диапазон дат отчета</div> <div>Диапазон дат отчета</div>
<DatePicker.RangePicker <DateRangeWrapper
disabledDate={disabledDate} disabledDate={disabledDate}
allowClear={false}
onCalendarChange={setFilterDateRange} onCalendarChange={setFilterDateRange}
showTime
value={filterDateRange} value={filterDateRange}
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
import { Table } from 'antd' import { Table } from 'antd'
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback, memo } from 'react'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -10,14 +10,14 @@ import { columns } from '@pages/Messages'
import '@styles/message.css' import '@styles/message.css'
export const ActiveMessagesOnline = ({ idWell }) => { export const ActiveMessagesOnline = memo(({ idWell }) => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
const [loader, setLoader] = useState(false) const [loader, setLoader] = useState(false)
const handleReceiveMessages = (messages) => { const handleReceiveMessages = useCallback((messages) => {
if (messages) if (messages)
setMessages(messages.items.splice(0, 4)) setMessages(messages.items.splice(0, 4))
} }, [])
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -30,7 +30,7 @@ export const ActiveMessagesOnline = ({ idWell }) => {
'Получение списка сообщений' 'Получение списка сообщений'
) )
return Subscribe('hubs/telemetry','ReceiveMessages', `well_${idWell}`, handleReceiveMessages) return Subscribe('hubs/telemetry','ReceiveMessages', `well_${idWell}`, handleReceiveMessages)
}, [idWell]) }, [idWell, handleReceiveMessages])
return ( return (
<LoaderPortal show={loader}> <LoaderPortal show={loader}>
@ -46,6 +46,6 @@ export const ActiveMessagesOnline = ({ idWell }) => {
/> />
</LoaderPortal> </LoaderPortal>
) )
} })
export default ActiveMessagesOnline export default ActiveMessagesOnline

View File

@ -1,9 +1,10 @@
import { memo } from 'react'
import { Popover } from 'antd' import { Popover } from 'antd'
import { ControlOutlined } from '@ant-design/icons' import { ControlOutlined } from '@ant-design/icons'
import { ValueDisplay } from '@components/Display' import { ValueDisplay } from '@components/Display'
export const ChartTimeOnlineFooter = ({ data, lineGroup }) => { export const ChartTimeOnlineFooter = memo(({ data, lineGroup }) => {
const getFooterData = (name) => { const getFooterData = (name) => {
const dataIdx = data && lineGroup?.find(line => line?.footer === name)?.xAccessorName const dataIdx = data && lineGroup?.find(line => line?.footer === name)?.xAccessorName
return (<ValueDisplay value={data?.[dataIdx]}/>) return (<ValueDisplay value={data?.[dataIdx]}/>)
@ -39,4 +40,6 @@ export const ChartTimeOnlineFooter = ({ data, lineGroup }) => {
</div> </div>
</div> </div>
) )
} })
export default ChartTimeOnlineFooter

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import { makeDateSorter } from '@components/Table' import { makeDateSorter } from '@components/Table'
@ -9,7 +9,7 @@ import { ChartTimeOnlineFooter } from './ChartTimeOnlineFooter'
const GetLimitShape = (flowChartData, points, accessor) => { const GetLimitShape = (flowChartData, points, accessor) => {
const min = [], max = [] const min = [], max = []
for (let point of points) { for (const point of points) {
const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd) const program = flowChartData.find(v => v.depthStart < point.depth && point.depth < v.depthEnd)
if (!program) continue if (!program) continue
@ -29,7 +29,9 @@ const RemoveSimilar = (input, accessor) => {
return data return data
} }
export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, pointCount = 2048, additionalLabels }) => { const addPointData = (point) => ({ depth: point.wellDepth })
export const MonitoringColumn = memo(({ lineGroup, data, flowChartData, interval, showBorder, style, headerHeight, pointCount = 2048, additionalLabels }) => {
const [dataStore, setDataStore] = useState([]) const [dataStore, setDataStore] = useState([])
const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([]) const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([])
const dataLast = data?.[data.length - 1] const dataLast = data?.[data.length - 1]
@ -41,9 +43,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
value: dataLast?.[line.xAccessorName] value: dataLast?.[line.xAccessorName]
})) }))
const addPointData = (point) => ({ depth: point.wellDepth }) const postParsing = useCallback((data) => {
const postParsing = (data) => {
lineGroupWithoutShapes.forEach(lineCfg => { lineGroupWithoutShapes.forEach(lineCfg => {
const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg) const lineDataSet = GetOrCreateDatasetByLineConfig(data.data, lineCfg)
@ -54,7 +54,7 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
}) })
} }
}) })
} }, [lineGroupWithoutShapes, flowChartData, lineGroup])
useEffect(() => { useEffect(() => {
setDataStore(prevData => { setDataStore(prevData => {
@ -119,6 +119,6 @@ export const MonitoringColumn = ({ lineGroup, data, flowChartData, interval, sho
<ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} /> <ChartTimeOnlineFooter data={dataLast} lineGroup={lineGroup} />
</div> </div>
) )
} })
export default MonitoringColumn export default MonitoringColumn

View File

@ -78,14 +78,17 @@ export const Setpoints = ({ idWell, ...other }) => {
</Button> </Button>
<Modal <Modal
width={1200} width={1200}
title={'Рекомендованные уставки'} title={(
<>
<span style={{ marginRight: '15px' }}>Рекомендованные уставки</span>
<Button onClick={() => setIsSenderVisible(true)} disabled={!hasPermission('Setpoints.edit')}>
Рекомендовать
</Button>
</>
)}
visible={isModalVisible} visible={isModalVisible}
onCancel={() => setIsModalVisible(false)} onCancel={() => setIsModalVisible(false)}
footer={( footer={null}
<Button onClick={() => setIsSenderVisible(true)} disabled={!hasPermission('Setpoints.edit')}>
Рекомендовать
</Button>
)}
> >
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>
<Table <Table
@ -94,6 +97,7 @@ export const Setpoints = ({ idWell, ...other }) => {
columns={historyColumns} columns={historyColumns}
dataSource={setpoints} dataSource={setpoints}
pagination={false} pagination={false}
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
/> />
</LoaderPortal> </LoaderPortal>
</Modal> </Modal>

View File

@ -1,5 +1,5 @@
import { Select } from 'antd' import { Select } from 'antd'
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
import { import {
DrillFlowChartService, DrillFlowChartService,
@ -49,7 +49,8 @@ const blockHeightGroup = [
yAccessorName: 'date', yAccessorName: 'date',
color: '#333', color: '#333',
showLine: false, showLine: false,
xConstValue: 30, showDatalabels: true,
xConstValue: 50,
dash dash
}, { }, {
label: 'Расход', label: 'Расход',
@ -310,19 +311,15 @@ export default function TelemetryView({ idWell }) {
const [flowChartData, setFlowChartData] = useState([]) const [flowChartData, setFlowChartData] = useState([])
const [rop, setRop] = useState(null) const [rop, setRop] = useState(null)
const handleDataSaub = (data) => { const handleDataSaub = useCallback((data) => {
if (data) { if (data) {
const dataSaub = normalizeData(data) const dataSaub = normalizeData(data)
dataSaub.sort(makeDateSorter('date')) dataSaub.sort(makeDateSorter('date'))
setDataSaub(dataSaub) setDataSaub(dataSaub)
} }
} }, [])
const handleDataSpin = (data) => { const handleDataSpin = useCallback((data) => data && setDataSpin(data), [])
if (data) {
setDataSpin(data)
}
}
useEffect(() => { useEffect(() => {
const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub) const unsubscribeSaub = Subscribe('hubs/telemetry', 'ReceiveDataSaub', `well_${idWell}`, handleDataSaub)
@ -344,7 +341,7 @@ export default function TelemetryView({ idWell }) {
unsubscribeSaub() unsubscribeSaub()
unsubscribeSpin() unsubscribeSpin()
} }
}, [idWell, chartInterval]) }, [idWell, chartInterval, handleDataSpin, handleDataSaub])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -358,18 +355,16 @@ export default function TelemetryView({ idWell }) {
'Получение данных по скважине' 'Получение данных по скважине'
), [idWell]) ), [idWell])
const onStatusChanged = (value) => { const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
invokeWebApiWrapperAsync( async () => {
async () => { const well = { ...wellData, idState: value }
const well = { ...wellData, idState: value } await WellService.updateWell(idWell, well)
await WellService.updateWell(idWell, well) setWellData(well)
setWellData(well) },
}, setShowLoader,
setShowLoader, `Не удалось задать состояние скважины "${idWell}"`,
`Не удалось задать состояние скважины "${idWell}"`, 'Задание состояния скважины'
'Задание состояния скважины' ), [idWell, wellData])
)
}
const columnAdditionalLabels = { const columnAdditionalLabels = {
1: rop && [ 1: rop && [

View File

@ -6,7 +6,7 @@ import {
FilePdfOutlined, FilePdfOutlined,
DatabaseOutlined, DatabaseOutlined,
ExperimentOutlined, ExperimentOutlined,
FundProjectionScreenOutlined, DeploymentUnitOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { Switch, useParams } from 'react-router-dom' import { Switch, useParams } from 'react-router-dom'
@ -17,12 +17,15 @@ import Report from './Report'
import Archive from './Archive' import Archive from './Archive'
import Measure from './Measure' import Measure from './Measure'
import Messages from './Messages' import Messages from './Messages'
import Analytics from './Analytics'
import Documents from './Documents' import Documents from './Documents'
import TelemetryView from './TelemetryView' import TelemetryView from './TelemetryView'
import WellOperations from './WellOperations' import WellOperations from './WellOperations'
import DrillingProgram from './DrillingProgram' import DrillingProgram from './DrillingProgram'
import TelemetryAnalysis from './TelemetryAnalysis' import TelemetryAnalysis from './TelemetryAnalysis'
import '@styles/index.css'
const { Content } = Layout const { Content } = Layout
export const Well = memo(() => { export const Well = memo(() => {
@ -35,6 +38,7 @@ export const Well = memo(() => {
<PrivateMenuItem.Link root={rootPath} key={'telemetry'} path={'telemetry'} icon={<FundViewOutlined />} title={'Мониторинг'}/> <PrivateMenuItem.Link root={rootPath} key={'telemetry'} path={'telemetry'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
<PrivateMenuItem.Link root={rootPath} key={'message'} path={'message'} icon={<AlertOutlined/>} title={'Сообщения'} /> <PrivateMenuItem.Link root={rootPath} key={'message'} path={'message'} icon={<AlertOutlined/>} title={'Сообщения'} />
<PrivateMenuItem.Link root={rootPath} key={'report'} path={'report'} icon={<FilePdfOutlined />} title={'Рапорт'} /> <PrivateMenuItem.Link root={rootPath} key={'report'} path={'report'} icon={<FilePdfOutlined />} title={'Рапорт'} />
<PrivateMenuItem.Link root={rootPath} key={'analytics'} path={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} />
<PrivateMenuItem.Link root={rootPath} key={'operations'} path={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} /> <PrivateMenuItem.Link root={rootPath} key={'operations'} path={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} />
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} /> <PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
{/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */} {/* <PrivateMenuItem.Link root={rootPath} key={'telemetryAnalysis'} path={'telemetryAnalysis'} icon={<FundProjectionScreenOutlined />} title={'Операции по телеметрии'} /> */}
@ -55,6 +59,9 @@ export const Well = memo(() => {
<PrivateRoute path={`${rootPath}/report`}> <PrivateRoute path={`${rootPath}/report`}>
<Report idWell={idWell} /> <Report idWell={idWell} />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={`${rootPath}/analytics/:tab?`}>
<Analytics idWell={idWell} rootPath={`${rootPath}/analytics`}/>
</PrivateRoute>
<PrivateRoute path={`${rootPath}/operations/:tab?`}> <PrivateRoute path={`${rootPath}/operations/:tab?`}>
<WellOperations idWell={idWell} /> <WellOperations idWell={idWell} />
</PrivateRoute> </PrivateRoute>
@ -77,6 +84,7 @@ export const Well = memo(() => {
`${rootPath}/telemetry`, `${rootPath}/telemetry`,
`${rootPath}/message`, `${rootPath}/message`,
`${rootPath}/report`, `${rootPath}/report`,
`${rootPath}/analytics`,
`${rootPath}/operations`, `${rootPath}/operations`,
`${rootPath}/archive`, `${rootPath}/archive`,
`${rootPath}/telemetryAnalysis`, `${rootPath}/telemetryAnalysis`,

View File

@ -62,6 +62,7 @@ export const DrillProcessFlow = memo(({ idWell }) => {
bordered bordered
columns={columns} columns={columns}
dataSource={flows} dataSource={flows}
tableName={'well_operations_flow'}
onRowAdd={hasPermission('DrillFlowChart.edit') && onAdd} onRowAdd={hasPermission('DrillFlowChart.edit') && onAdd}
onRowEdit={hasPermission('DrillFlowChart.edit') && onEdit} onRowEdit={hasPermission('DrillFlowChart.edit') && onEdit}
onRowDelete={hasPermission('DrillFlowChart.delete') && onDelete} onRowDelete={hasPermission('DrillFlowChart.delete') && onDelete}

View File

@ -21,7 +21,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
} }
return ( return (
<> <div>
<Tooltip title={'Импорт - загрузить файл с операциями на сервер'}> <Tooltip title={'Импорт - загрузить файл с операциями на сервер'}>
<Button <Button
disabled={!hasPermission('WellOperation.edit') || disabled} disabled={!hasPermission('WellOperation.edit') || disabled}
@ -50,7 +50,7 @@ export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
> >
<ImportOperations idWell={idWell} onDone={onDone} /> <ImportOperations idWell={idWell} onDone={onDone} />
</Modal> </Modal>
</> </div>
) )
}) })

View File

@ -16,8 +16,7 @@ import ChartDataLabels from 'chartjs-plugin-datalabels'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { getOperations } from '@utils/functions'
import { getOperations } from '@pages/Cluster/functions'
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin) Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)

View File

@ -71,6 +71,7 @@ export const WellDrillParams = memo(({ idWell }) => {
bordered bordered
columns={columns} columns={columns}
dataSource={params} dataSource={params}
tableName={'well_drill_params'}
onRowAdd={hasPermission('DrillParams.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление режима бурения')} onRowAdd={hasPermission('DrillParams.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление режима бурения')}
onRowEdit={hasPermission('DrillParams.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование режима бурения')} onRowEdit={hasPermission('DrillParams.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование режима бурения')}
onRowDelete={hasPermission('DrillParams.delete') && makeActionHandler('delete', handlerProps, recordParser, 'Удаление режима бурения')} onRowDelete={hasPermission('DrillParams.delete') && makeActionHandler('delete', handlerProps, recordParser, 'Удаление режима бурения')}

View File

@ -69,9 +69,10 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] })) sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] }))
setColumns(preColumns => { setColumns(preColumns => {
preColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 }) const newColumns = [...preColumns]
preColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 }) newColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
return preColumns newColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
return newColumns
}) })
}, },
setShowLoader, setShowLoader,
@ -131,6 +132,7 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
total: paginationTotal, total: paginationTotal,
onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize }) onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
}} }}
tableName={'well_operationse_editor'}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -63,6 +63,7 @@ export const WellSectionsStat = memo(({ idWell }) => {
size={'small'} size={'small'}
columns={columns} columns={columns}
dataSource={sections} dataSource={sections}
tableName={'well_operations_sections'}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -1,11 +1,10 @@
import { memo } from 'react' import { memo, useCallback } from 'react'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { Switch, useParams, useHistory } from 'react-router-dom' import { Switch, useParams, useHistory, useLocation } from 'react-router-dom'
import { import {
BarChartOutlined, BarChartOutlined,
BuildOutlined, BuildOutlined,
ControlOutlined, ControlOutlined,
DeploymentUnitOutlined,
LineChartOutlined, LineChartOutlined,
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
@ -17,32 +16,36 @@ import { ImportExportBar } from './ImportExportBar'
import { WellDrillParams } from './WellDrillParams' import { WellDrillParams } from './WellDrillParams'
import { DrillProcessFlow } from './DrillProcessFlow' import { DrillProcessFlow } from './DrillProcessFlow'
import { WellSectionsStat } from './WellSectionsStat' import { WellSectionsStat } from './WellSectionsStat'
import { WellCompositeEditor } from './WellCompositeEditor'
import { WellOperationsEditor } from './WellOperationsEditor' import { WellOperationsEditor } from './WellOperationsEditor'
import { Flex } from '@asb/components/Grid'
const { Content } = Layout const { Content } = Layout
export const WellOperations = memo(({ idWell }) => { export const WellOperations = memo(({ idWell }) => {
const { tab } = useParams() const { tab } = useParams()
const history = useHistory() const history = useHistory()
const location = useLocation()
const rootPath = `/well/${idWell}/operations` const rootPath = `/well/${idWell}/operations`
const onImported = () => history.push(`${rootPath}`) const onImported = useCallback(() =>
history.push({ pathname: `${rootPath}`, state: { from: location.pathname }})
, [history, location, rootPath])
const isIEBarDisabled = !['plan', 'fact'].includes(tab) const isIEBarDisabled = !['plan', 'fact'].includes(tab)
return( return(
<> <>
<Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}> <Flex style={{ width: '100%' }}>
<PrivateMenuItemLink root={rootPath} icon={<LineChartOutlined />} key={'tvd'} path={'tvd'} title={'TVD'} /> <Menu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]} style={{ flex: 1 }}>
<PrivateMenuItemLink root={rootPath} icon={<BuildOutlined />} key={'sections'} path={'sections'} title={'Секции'} /> <PrivateMenuItemLink root={rootPath} icon={<LineChartOutlined />} key={'tvd'} path={'tvd'} title={'TVD'} />
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'plan'} path={'plan'} title={'План'} /> <PrivateMenuItemLink root={rootPath} icon={<BuildOutlined />} key={'sections'} path={'sections'} title={'Секции'} />
<PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'fact'} path={'fact'} title={'Факт'} /> <PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'plan'} path={'plan'} title={'План'} />
<PrivateMenuItemLink root={rootPath} icon={<BarChartOutlined />} key={'drillProcessFlow'} path={'drillProcessFlow'} title={'РТК'} /> <PrivateMenuItemLink root={rootPath} icon={<TableOutlined />} key={'fact'} path={'fact'} title={'Факт'} />
<PrivateMenuItemLink root={rootPath} icon={<ControlOutlined />} key={'params'} path={'params'} title={'Режимы'} /> <PrivateMenuItemLink root={rootPath} icon={<BarChartOutlined />} key={'drillProcessFlow'} path={'drillProcessFlow'} title={'РТК'} />
<PrivateMenuItemLink root={rootPath} icon={<DeploymentUnitOutlined />} key={'composite'} path={'composite'} title={'Аналитика'} /> <PrivateMenuItemLink root={rootPath} icon={<ControlOutlined />} key={'params'} path={'params'} title={'Режимы'} />
</Menu>
<ImportExportBar idWell={idWell} disabled={isIEBarDisabled} onImported={onImported}/> <ImportExportBar idWell={idWell} disabled={isIEBarDisabled} onImported={onImported}/>
</Menu> </Flex>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<Switch> <Switch>
@ -53,7 +56,7 @@ export const WellOperations = memo(({ idWell }) => {
<WellSectionsStat idWell={idWell}/> <WellSectionsStat idWell={idWell}/>
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={`${rootPath}/plan`}> <PrivateRoute path={`${rootPath}/plan`}>
<WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'} showSettingsChanger /> <WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'}/>
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={`${rootPath}/fact`}> <PrivateRoute path={`${rootPath}/fact`}>
<WellOperationsEditor idWell={idWell} idType={1} tableName={'well_operations_fact'}/> <WellOperationsEditor idWell={idWell} idType={1} tableName={'well_operations_fact'}/>
@ -64,17 +67,13 @@ export const WellOperations = memo(({ idWell }) => {
<PrivateRoute path={`${rootPath}/params`}> <PrivateRoute path={`${rootPath}/params`}>
<WellDrillParams idWell={idWell}/> <WellDrillParams idWell={idWell}/>
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={`${rootPath}/composite/:tab?`}>
<WellCompositeEditor idWell={idWell}/>
</PrivateRoute>
<PrivateDefaultRoute urls={[ <PrivateDefaultRoute urls={[
`${rootPath}/plan`, `${rootPath}/plan`,
`${rootPath}/fact`, `${rootPath}/fact`,
`${rootPath}/tvd`, `${rootPath}/tvd`,
`${rootPath}/sections`, `${rootPath}/sections`,
`${rootPath}/drillProcessFlow`, `${rootPath}/drillProcessFlow`,
`${rootPath}/params`, `${rootPath}/params`
`${rootPath}/composite`
]}/> ]}/>
</Switch> </Switch>
</Content> </Content>

View File

@ -1,13 +1,13 @@
const reportWebVitals = onPerfEntry => { export const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry)
getFID(onPerfEntry); getFID(onPerfEntry)
getFCP(onPerfEntry); getFCP(onPerfEntry)
getLCP(onPerfEntry); getLCP(onPerfEntry)
getTTFB(onPerfEntry); getTTFB(onPerfEntry)
}); })
} }
}; }
export default reportWebVitals; export default reportWebVitals

View File

@ -14,7 +14,20 @@
//@layout-header-background: rgb(195, 40,40); //@layout-header-background: rgb(195, 40,40);
@layout-header-background: rgb(65, 63, 61); @layout-header-background: rgb(65, 63, 61);
#root, .app{min-height:100%;} @header-height: 64px;
@layout-min-height: calc(100vh - @header-height);
#root, .app{
min-height:100%;
}
.ant-layout{
flex: 1;
> .ant-menu {
flex: 0;
}
}
html { html {
display: flex; display: flex;
@ -29,14 +42,15 @@ html {
margin-left: 30px; margin-left: 30px;
} }
.login_page{ .login_page {
position: absolute; position: absolute;
height:100%; height: 100%;
width:100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 24px; padding: 24px;
background-color: #9d9d9d;
} }
.shadow{ .shadow{
@ -48,15 +62,13 @@ html {
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
gap: 50px; gap: 50px;
height: @header-height;
} }
.header .logo { .header .logo {
background-color: rgb(230, 230, 230);
border-radius: 32px;
padding: 8px 24px; padding: 8px 24px;
margin: 0 10px; margin: 0 10px;
margin-bottom: 2px; margin-bottom: 2px;
box-shadow: 0 0 2px #fff;
} }
.header .title{ .header .title{
@ -100,9 +112,15 @@ html {
margin-right: 2px; margin-right: 2px;
} }
.ant-layout-content {
display: flex;
flex-direction: column;
align-items: stretch;
}
.sheet{ .sheet{
padding: 5px 24px; padding: 5px 24px;
min-height: 280px; min-height: calc(@layout-min-height - 15px); // 280px;
margin: 0 15px 15px 15px; margin: 0 15px 15px 15px;
} }

View 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; }
}

View File

@ -72,7 +72,11 @@ body {
} }
.h-100vh { .h-100vh {
height: 100vh; height: calc(100vh - 64px);
}
.p-10 {
padding: 10px;
} }
.vertical-align-center { .vertical-align-center {
@ -115,3 +119,11 @@ code {
margin: auto; margin: auto;
} }
.first-column-title {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
position: relative;
padding: 16px 0;
}

View 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;
}
}
}

View File

@ -1,5 +1,7 @@
import moment from 'moment' import moment from 'moment'
import { SimpleTimezoneDto } from '@api'
export type RawDate = number | string | Date export type RawDate = number | string | Date
export const defaultFormat: string = 'YYYY.MM.DD HH:mm' export const defaultFormat: string = 'YYYY.MM.DD HH:mm'
@ -38,7 +40,40 @@ export const periodToString = (time?: number) => {
return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}` return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
} }
export const calcDuration = (start: RawDate, end: RawDate) => { export const calcDuration = (start: unknown, end: unknown) => {
if (!isRawDate(start) || !isRawDate(end)) return if (!isRawDate(start) || !isRawDate(end)) return
return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day
} }
export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate => {
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return NaN
const d = new Date(date)
d.setMilliseconds(d.getMilliseconds() + value * timeInS[type] * 1000)
return d
}
export const rawTimezones = {
'Калининград': 2,
'Москва': 3,
'Самара': 4,
'Екатеринбург': 5,
'Омск': 6,
'Красноярск': 7,
'Новосибирск': 7,
'Иркутск': 8,
'Чита': 9,
'Владивосток': 10,
'Магадан': 11,
'Южно-Сахалинск': 11,
'Среднеколымск': 11,
'Анадырь': 12,
'Петропавловск-Камчатский': 12,
}
export type TimezoneId = keyof typeof rawTimezones
export const isTimezoneId = (value: unknown): value is TimezoneId => !!value && String(value) in rawTimezones
export const findTimezoneId = (value: SimpleTimezoneDto): TimezoneId =>
(isTimezoneId(value.timezoneId) && value.timezoneId) ||
(Object.keys(rawTimezones) as TimezoneId[]).find(id => rawTimezones[id] === value.hours) as TimezoneId

86
src/utils/functions.tsx Normal file
View 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)
})
}

View File

@ -9,3 +9,10 @@ export const mainFrameSize = () => ({
}) })
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
/**
* Объединить типы, исключив совпадающие поля справа
* @param T Тип, передаваемый полностью
* @param R Аддитивный тип
*/
export type OmitExtends<T, R> = T & Omit<R, keyof T>

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