Merge branch 'dev'

This commit is contained in:
ngfrolov 2022-03-29 09:19:48 +05:00
commit fd6c36b6d5
69 changed files with 1352 additions and 708 deletions

View File

@ -2,7 +2,7 @@ import { memo, ReactNode } from 'react'
import { Layout, LayoutProps } from 'antd' import { Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader' import PageHeader from '@components/PageHeader'
import WellTreeSelector from '@components/WellTreeSelector' import WellTreeSelector from '@components/selectors/WellTreeSelector'
export type LayoutPortalProps = LayoutProps & { export type LayoutPortalProps = LayoutProps & {
title?: ReactNode title?: ReactNode

View File

@ -0,0 +1,25 @@
import { ReactNode } from 'react'
import { formatDate } from '@utils'
import makeColumn, { columnPropsOther } from '.'
import { DatePickerWrapper, makeDateSorter } from '..'
export const makeDateColumn = (
title: ReactNode,
key: string,
utc?: boolean,
format?: string,
other?: columnPropsOther
) => makeColumn(title, key, {
...other,
render: (date) => (
<div className={'text-align-r-container'}>
<span>{formatDate(date, utc, format) ?? '-'}</span>
</div>
),
sorter: makeDateSorter(key),
input: <DatePickerWrapper />,
})
export default makeDateColumn

View File

@ -2,6 +2,7 @@ import { ReactNode } from 'react'
import { Rule } from 'antd/lib/form' import { Rule } from 'antd/lib/form'
import { ColumnProps } from 'antd/lib/table' import { ColumnProps } from 'antd/lib/table'
export { makeDateColumn } from './date'
export { export {
RegExpIsFloat, RegExpIsFloat,
makeNumericRender, makeNumericRender,

View File

@ -14,8 +14,8 @@ export const makeSelectColumn = <T extends unknown = string>(
...other, ...other,
input: <Select options={options} {...selectOther}/>, input: <Select options={options} {...selectOther}/>,
render: (value) => { render: (value) => {
const item = options?.find(option => option?.value === value) const item = options?.find(option => option?.value === value)
return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--' return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--'
} }
}) })

View File

@ -6,8 +6,8 @@ import moment, { Moment } from 'moment'
import { defaultFormat } from '@utils' import { defaultFormat } from '@utils'
export type DatePickerWrapperProps = PickerDateProps<Moment> & { export type DatePickerWrapperProps = PickerDateProps<Moment> & {
value: Moment, value?: Moment,
onChange: (date: Moment | null) => any onChange?: (date: Moment | null) => any
isUTC?: boolean isUTC?: boolean
} }
@ -17,7 +17,7 @@ export const DatePickerWrapper = memo<DatePickerWrapperProps>(({ value, onChange
allowClear={false} allowClear={false}
format={defaultFormat} format={defaultFormat}
defaultValue={moment()} defaultValue={moment()}
onChange={(date) => onChange(date)} onChange={(date) => onChange?.(date)}
value={isUTC ? moment.utc(value).local() : moment(value)} value={isUTC ? moment.utc(value).local() : moment(value)}
{...other} {...other}
/> />

View File

@ -188,6 +188,7 @@ export const EditableTable = memo(({
return { return {
...col, ...col,
onCell: (record) => ({ onCell: (record) => ({
...col.onCell?.(record),
editing: isEditing(record), editing: isEditing(record),
record, record,
dataIndex: col.dataIndex ?? col.key, dataIndex: col.dataIndex ?? col.key,

View File

@ -6,6 +6,7 @@ export {
RegExpIsFloat, RegExpIsFloat,
timezoneOptions, timezoneOptions,
TimezoneSelect, TimezoneSelect,
makeDateColumn,
makeGroupColumn, makeGroupColumn,
makeColumn, makeColumn,
makeColumnsPlanFact, makeColumnsPlanFact,

View File

@ -0,0 +1,34 @@
import { Select, SelectProps } from 'antd'
import { memo } from 'react'
import { getTelemetryLabel } from '@components/views'
import { TelemetryDto } from '@api'
import '@styles/components/telemetry_select.less'
export type TelemetrySelectProps = SelectProps & {
telemetry?: TelemetryDto[],
value?: TelemetryDto ,
onChange?: (value?: TelemetryDto) => void,
}
export const TelemetrySelect = memo<TelemetrySelectProps>(({ telemetry, value, onChange, ...other }) => (
<Select
allowClear
value={value?.id}
onChange={(id) => onChange?.(telemetry?.find((row) => row.id === id))}
className={'telemetry_select'}
dropdownClassName={'telemetry_select'}
{...other}
>
{telemetry?.map((row, i) => (
<Select.Option key={i} value={row.id}>
<span className={row?.info?.well ? 'telemetry_used' : 'telemetry_unused'}>
{getTelemetryLabel(row)}
</span>
</Select.Option>
))}
</Select>
))
export default TelemetrySelect

View File

@ -7,9 +7,9 @@ import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
import { LabelInValueType } from 'rc-select/lib/Select' import { LabelInValueType } from 'rc-select/lib/Select'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import LoaderPortal from './LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { WellIcon, WellIconState } from './icons' import { WellIcon, WellIconState } from '@components/icons'
import { invokeWebApiWrapperAsync } from './factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DepositService, DepositDto } from '@api' import { DepositService, DepositDto } from '@api'
import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg' import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg'

View File

@ -4,7 +4,7 @@ import { Tooltip } from 'antd'
import { TelemetryDto, TelemetryInfoDto } from '@api' import { TelemetryDto, TelemetryInfoDto } from '@api'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
const lables: Record<string, string> = { export const lables: Record<string, string> = {
timeZoneId: 'Временная зона', timeZoneId: 'Временная зона',
timeZoneOffsetTotalHours: 'Сдвиг временной зоны', timeZoneOffsetTotalHours: 'Сдвиг временной зоны',
drillingStartDate: 'Начало бурения', drillingStartDate: 'Начало бурения',

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
@ -9,7 +10,6 @@ import {
defaultPagination, defaultPagination,
makeTimezoneColumn makeTimezoneColumn
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api' import { AdminClusterService, AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -22,8 +22,16 @@ 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)
const [searchValue, setSearchValue] = useState('')
const clusterColumns = [ const filteredClusters = useMemo(() => clusters.filter((cluster) => cluster && (!searchValue || [
cluster.caption ?? '',
cluster.latitude?.toString ?? '',
cluster.longitude?.toString() ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [clusters, searchValue])
const clusterColumns = useMemo(() => [
makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', {
width: 200, width: 200,
editable: true, editable: true,
@ -38,7 +46,7 @@ export const ClusterController = memo(() => {
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 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ], [deposits])
const updateTable = useCallback(() => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -63,27 +71,35 @@ export const ClusterController = memo(() => {
useEffect(updateTable, [updateTable]) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminClusterService, service: AdminClusterService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable, onComplete: updateTable,
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
size={'small'}
bordered bordered
dataSource={clusters} size={'small'}
loading={showLoader}
columns={clusterColumns} columns={clusterColumns}
dataSource={filteredClusters}
pagination={defaultPagination} pagination={defaultPagination}
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'} tableName={'admin_cluster_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
@ -8,7 +9,6 @@ import {
makeSelectColumn, makeSelectColumn,
defaultPagination defaultPagination
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api' import { AdminCompanyService, AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -20,6 +20,11 @@ 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 [searchValue, setSearchValue] = useState('')
const filteredCompanies = useMemo(() => companies.filter((company) => company && (!searchValue ||
company.caption?.toLowerCase()?.includes(searchValue.toLowerCase())
)), [companies, searchValue])
const updateTable = useCallback(async () => { const updateTable = useCallback(async () => {
const companies = await AdminCompanyService.getAll() const companies = await AdminCompanyService.getAll()
@ -53,7 +58,7 @@ export const CompanyController = memo(() => {
'Получение списка типов команд' 'Получение списка типов команд'
), [updateTable]) ), [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminCompanyService, service: AdminCompanyService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
@ -63,22 +68,30 @@ export const CompanyController = memo(() => {
`Не удалось обновить список компаний`, `Не удалось обновить список компаний`,
'Получение списка компаний' 'Получение списка компаний'
), ),
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полю Название)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={companies} loading={showLoader}
dataSource={filteredCompanies}
pagination={defaultPagination} pagination={defaultPagination}
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'} tableName={'admin_company_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
@ -7,7 +8,6 @@ import {
makeStringSorter, makeStringSorter,
defaultPagination defaultPagination
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api' import { AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -26,6 +26,11 @@ const columns = [
export const CompanyTypeController = memo(() => { export const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([]) const [companyTypes, setCompanyTypes] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
const filteredCompanyTypes = useMemo(() => companyTypes.filter((companyType) => companyType && (!searchValue ||
companyType.caption?.toLowerCase()?.includes(searchValue.toLowerCase())
)), [companyTypes, searchValue])
const updateTable = useCallback(() => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async() => { async() => {
@ -39,27 +44,35 @@ export const CompanyTypeController = memo(() => {
useEffect(updateTable, [updateTable]) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminCompanyTypeService, service: AdminCompanyTypeService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable, onComplete: updateTable,
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полю Название)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
bordered bordered
size={'small'} size={'small'}
columns={columns} columns={columns}
dataSource={companyTypes} loading={showLoader}
pagination={defaultPagination} pagination={defaultPagination}
dataSource={filteredCompanyTypes}
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'} tableName={'admin_company_type_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,12 +1,12 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table' import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table'
import { AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
import { min1 } from '@utils/validationRules'
import { arrayOrDefault } from '@utils'
import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-' export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
@ -20,6 +20,14 @@ const depositColumns = [
export const DepositController = memo(() => { export const DepositController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
const filteredDeposits = useMemo(() => deposits.filter((deposit) => deposit && (!searchValue || [
deposit.caption ?? '',
deposit.latitude?.toString() ?? '',
deposit.longitude?.toString() ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [deposits, searchValue])
const updateTable = useCallback(() => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async() => { async() => {
@ -33,27 +41,35 @@ export const DepositController = memo(() => {
useEffect(updateTable, [updateTable]) useEffect(updateTable, [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminDepositService, service: AdminDepositService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable, onComplete: updateTable,
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
size={'small'}
bordered bordered
dataSource={deposits} size={'small'}
loading={showLoader}
columns={depositColumns} columns={depositColumns}
dataSource={filteredDeposits}
pagination={defaultPagination} pagination={defaultPagination}
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'} tableName={'admin_deposit_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
@ -6,7 +7,6 @@ import {
makeColumn, makeColumn,
makeStringSorter makeStringSorter
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminPermissionService } from '@api' import { AdminPermissionService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -27,8 +27,15 @@ const columns = [
] ]
export const PermissionController = memo(() => { export const PermissionController = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
const filteredPermissions = useMemo(() => permissions.filter((permission) => permission && (!searchValue || [
permission.name ?? '',
permission.description ?? '',
].join(' ').includes(searchValue.toLowerCase()))
), [permissions, searchValue])
const updateTable = useCallback(async () => invokeWebApiWrapperAsync( const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
async () => { async () => {
@ -42,27 +49,35 @@ export const PermissionController = memo(() => {
useEffect(() => updateTable(), [updateTable]) useEffect(() => updateTable(), [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminPermissionService, service: AdminPermissionService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable onComplete: updateTable
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Описание)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
bordered bordered
size={'small'} size={'small'}
columns={columns} columns={columns}
dataSource={permissions} loading={showLoader}
dataSource={filteredPermissions}
pagination={{ showSizeChanger: true }} pagination={{ showSizeChanger: true }}
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'} tableName={'admin_permission_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,9 +1,9 @@
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import LoaderPortal from '@components/LoaderPortal'
import { PermissionView, RoleView } from '@components/views' import { PermissionView, RoleView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeActionHandler, makeColumn, makeTagColumn } from '@components/Table' import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table'
import { AdminPermissionService, AdminUserRoleService } from '@api' import { AdminPermissionService, AdminUserRoleService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
@ -13,29 +13,31 @@ export const RoleController = memo(() => {
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [roles, setRoles] = useState([]) const [roles, setRoles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([]) const [searchValue, setSearchValue] = useState('')
const filteredRoles = useMemo(() => roles.filter((role) => role && (!searchValue ||
role.caption?.toLowerCase()?.includes(searchValue.toLowerCase())
)), [roles, searchValue])
const columns = useMemo(() => [
makeTextColumn('Название', 'caption', null, null, null, { width: 100, editable: true, formItemRules: min1 }),
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
width: 400,
editable: true,
render: (role) => <RoleView role={role} />
}, { allowClear: true }),
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
width: 600,
editable: true,
render: (permission) => <PermissionView info={permission} />,
}),
], [roles, permissions])
const loadRoles = useCallback(async () => { const loadRoles = useCallback(async () => {
const roles = await AdminUserRoleService.getAll() const roles = await AdminUserRoleService.getAll()
setRoles(arrayOrDefault(roles)) setRoles(arrayOrDefault(roles))
}, []) }, [])
useEffect(() => {
setColumns([
makeColumn('Название', 'caption', { width: 100, editable: true, formItemRules: min1 }),
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
width: 400,
editable: true,
render: (role) => <RoleView role={role} />
}, { allowClear: true }),
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
width: 600,
editable: true,
render: (permission) => <PermissionView info={permission} />,
}),
])
}, [roles, permissions])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
const permissions = await AdminPermissionService.getAll() const permissions = await AdminPermissionService.getAll()
@ -47,7 +49,7 @@ export const RoleController = memo(() => {
'Получение списка ролей' 'Получение списка ролей'
), [loadRoles]) ), [loadRoles])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminUserRoleService, service: AdminUserRoleService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
@ -55,23 +57,31 @@ export const RoleController = memo(() => {
loadRoles, loadRoles,
setShowLoader, setShowLoader,
`Не удалось загрузить список ролей`, `Не удалось загрузить список ролей`,
'Получение списка ролей' 'Получение списка ролей',
) )
} }), [loadRoles])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полю Название)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
bordered bordered
size={'small'} size={'small'}
columns={columns} columns={columns}
dataSource={roles} loading={showLoader}
dataSource={filteredRoles}
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'} tableName={'admin_role_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -0,0 +1,136 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { PullRequestOutlined } from '@ant-design/icons'
import { Button, Descriptions, Popconfirm } from 'antd'
import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal'
import { lables } from '@components/views/TelemetryView'
import { invokeWebApiWrapperAsync } from '@components/factory'
import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
const { Item } = Descriptions
export const TelemetryInfo = memo(({ info, danger, ...other }) => (
<Descriptions
bordered
column={1}
size={'small'}
style={{ background: 'white' }}
className={'telemetry_merger_info'}
{...other}
>
{Object.keys({ ...lables, ...info }).map(key => (
<Item
key={key}
label={lables[key] ?? key}
style={{ color: danger === true || danger?.includes(key) ? 'red' : 'black' }}
>{info?.[key] ?? '-'}</Item>
))}
</Descriptions>
))
export const TelemetryMerger = memo(() => {
const [primary, setPrimary] = useState(null)
const [secondary, setSecondary] = useState(null)
const [telemetry, setTelemetry] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [isMerging, setIsMerging] = useState(false)
const location = useLocation()
const danger = useMemo(() => [
primary?.info?.well !== secondary?.info?.well && 'well',
primary?.info?.cluster !== secondary?.info?.cluster && 'cluster',
primary?.info?.deposit !== secondary?.info?.deposit && 'deposit',
], [primary, secondary])
const updateTelemetry = useCallback(async () => await invokeWebApiWrapperAsync(
async () => {
const telemetry = arrayOrDefault(await AdminTelemetryService.getAll())
setTelemetry(telemetry)
},
setIsLoading,
'Не удалось загрузить список телеметрий',
'Получение списка телеметрий',
), [])
const mergeTelemetry = useCallback(() => invokeWebApiWrapperAsync(
async () => {
await new Promise(res => setTimeout(res, 1000))
/// await AdminTelemetryService.mergeTelemetries(secondary.id, primary.id)
await updateTelemetry()
},
setIsMerging,
'Не удалось объединить телеметрии',
'Объединение телеметрий',
), [updateTelemetry])
useEffect(() => updateTelemetry(), [updateTelemetry])
useEffect(() => {
const query = new URLSearchParams(location.search)
const primaryId = parseInt(query.get('primary') ?? null)
const secondaryId = parseInt(query.get('secondary') ?? null)
const primary = isNaN(primaryId) ? null : telemetry.find((t) => t.id === primaryId)
const secondary = isNaN(secondaryId) ? null : telemetry.find((t) => t.id === secondaryId)
console.log([primary, secondary])
setPrimary(primary)
setSecondary(secondary)
}, [location, telemetry])
return (
<LoaderPortal show={isLoading}>
<div className={'description'} style={{ marginTop: '15px' }}>{
///TODO: Добавить описание
}</div>
<Grid>
<GridItem col={1} row={1}>Результирующая телеметрия</GridItem>
<GridItem col={2} row={1}>Исходная телеметрия</GridItem>
<GridItem col={1} row={2}>
<TelemetrySelect
value={primary}
disabled={isMerging}
telemetry={telemetry}
onChange={setPrimary}
style={{ width: '100%', marginRight: '15px' }}
/>
</GridItem>
<GridItem col={2} row={2}>
<TelemetrySelect
value={secondary}
disabled={isMerging}
telemetry={telemetry}
onChange={setSecondary}
style={{ width: '100%' }}
/>
</GridItem>
<GridItem col={3} row={2}>
<Popconfirm
disabled={isMerging || !primary || !secondary}
title={'Исходная телеметрия будет удалена после объединения. Вы уверены?'}
okText={'Объединить'}
onConfirm={mergeTelemetry}
>
<Button
type={'primary'}
icon={<PullRequestOutlined />}
disabled={!primary || !secondary}
loading={isMerging}
>Объединить</Button>
</Popconfirm>
</GridItem>
<GridItem col={1} row={3}>
<TelemetryInfo info={primary?.info} danger={danger} />
</GridItem>
<GridItem col={2} row={3}>
<TelemetryInfo info={secondary?.info} danger={danger} />
</GridItem>
</Grid>
</LoaderPortal>
)
})
export default TelemetryMerger

View File

@ -0,0 +1,121 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { PullRequestOutlined } from '@ant-design/icons'
import { Button, Input } from 'antd'
import {
defaultPagination,
makeColumn,
makeDateSorter,
makeNumericColumn,
makeNumericRender,
makeTextColumn,
Table
} from '@components/Table'
import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
import { useHistory } from 'react-router-dom'
export const TelemetryController = memo(() => {
const [telemetryData, setTelemetryData] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
const history = useHistory()
const toMerger = useCallback((type, id) => () => history.push(`/admin/telemetry/merger/?${type}=${id}`), [history])
const mergeRender = useCallback((value, record) => (
<Poprompt
placement={'topLeft'}
buttonProps={{
icon: <PullRequestOutlined />,
size: 'small',
danger: !!value,
}}
footer={(
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={toMerger('primary', record.id)}>Основная</Button>
<Button onClick={toMerger('secondary', record.id)} style={{ marginLeft: '8px' }}>Сливаемая</Button>
</div>
)}
>
<p>Вы собираетесь использовать данную телеметрию для слияния</p>
{record.realWell && (
<p style={{ color: 'red' }}>Внимание! Телеметрии назначена скважина!</p>
)}
</Poprompt>
), [toMerger])
const columns = useMemo(() => [
makeColumn('', 'hasParent', { render: mergeRender }),
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)),
makeTextColumn('UID', 'remoteUid'),
makeTextColumn('Назначена на скважину', 'realWell'),
makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')),
makeTextColumn('Часовой пояс', 'timeZoneId'),
makeTextColumn('Скважина', 'well'),
makeTextColumn('Куст', 'cluster'),
makeTextColumn('Месторождение', 'deposit'),
makeTextColumn('Заказчик', 'customer'),
makeTextColumn('Комментарий', 'comment'),
makeTextColumn('Версия HMI', 'hmiVersion'),
makeTextColumn('Версия САУБ', 'saubPlcVersion'),
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
], [mergeRender])
const filteredTelemetryData = useMemo(() => telemetryData.filter((telemetry) => telemetry && (!searchValue || [
telemetry.id?.toString() ?? '',
telemetry.remoteUid ?? '',
telemetry.realWell ?? '',
telemetry.drillingStartDate ?? '',
telemetry.well ?? '',
telemetry.cluster ?? '',
telemetry.deposit ?? '',
telemetry.customer ?? '',
telemetry.comment ?? '',
telemetry.hmiVersion ?? '',
telemetry.saubPlcVersion ?? '',
telemetry.spinPlcVersion ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [telemetryData, searchValue])
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll())
setTelemetryData(telemetryData.map((telemetry) => ({
...(telemetry?.info ?? []),
id: telemetry?.id,
remoteUid: telemetry?.remoteUid,
realWell: telemetry?.well?.caption,
})))
},
setShowLoader,
`Не удалось загрузить список телеметрии скважин`,
'Полученик списка телеметрии скважин'
), [])
return (
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по всем полям)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<Table
bordered
size={'small'}
columns={columns}
loading={showLoader}
pagination={defaultPagination}
dataSource={filteredTelemetryData}
tableName={'admin_telemetry_controller'}
/>
</>
)
})
export default TelemetryController

View File

@ -0,0 +1,42 @@
import { Layout, Menu } from 'antd'
import { lazy, memo, Suspense } from 'react'
import { Switch, useParams } from 'react-router-dom'
import { PrivateMenuItem, PrivateRoute, PrivateDefaultRoute } from '@components/Private'
import { SuspenseFallback } from '@pages/SuspenseFallback'
const TelemetryViewer = lazy(() => import('./TelemetryViewer'))
const TelemetryMerger = lazy(() => import('./TelemetryMerger'))
const rootPath = '/admin/telemetry'
export const Telemetry = memo(() => {
const { tab } = useParams()
return (
<Layout>
<Menu mode={'horizontal'} selectable={true} selectedKeys={[tab]}>
<PrivateMenuItem.Link root={rootPath} key={'viewer'} path={'viewer'} title={'Просмотр'} />
<PrivateMenuItem.Link root={rootPath} key={'merger'} path={'merger'} title={'Объединение'} />
</Menu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}>
<Switch>
<PrivateRoute path={`${rootPath}/viewer`} component={TelemetryViewer} />
<PrivateRoute path={`${rootPath}/merger`} component={TelemetryMerger} />
<PrivateDefaultRoute urls={[
`${rootPath}/viewer`,
`${rootPath}/merger`,
]}/>
</Switch>
</Suspense>
</Layout.Content>
</Layout>
</Layout>
)
})
export default Telemetry

View File

@ -1,65 +0,0 @@
import { memo, useEffect, useState } from 'react'
import {
defaultPagination,
makeDateSorter,
makeNumericColumn,
makeNumericRender,
makeTextColumn,
Table
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
const columns = [
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)),
makeTextColumn('UID', 'remoteUid'),
makeTextColumn('Назначена на скважину', 'realWell'),
makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')),
makeTextColumn('Часовой пояс', 'timeZoneId'),
makeTextColumn('Скважина', 'well'),
makeTextColumn('Куст', 'cluster'),
makeTextColumn('Месторождение', 'deposit'),
makeTextColumn('Заказчик', 'customer'),
makeTextColumn('Комментарий', 'comment'),
makeTextColumn('Версия HMI', 'hmiVersion'),
makeTextColumn('Версия САУБ', 'saubPlcVersion'),
makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'),
]
export const TelemetryController = memo(() => {
const [telemetryData, setTelemetryData] = useState([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll())
setTelemetryData(telemetryData.map((telemetry) => ({
...(telemetry?.info ?? []),
id: telemetry?.id,
remoteUid: telemetry?.remoteUid,
realWell: telemetry?.well?.caption,
})))
},
setIsLoading,
`Не удалось загрузить список телеметрии скважин`,
'Полученик списка телеметрии скважин'
), [])
return (
<LoaderPortal show={isLoading}>
<Table
size={'small'}
bordered
columns={columns}
dataSource={telemetryData}
pagination={defaultPagination}
tableName={'admin_telemetry_controller'}
/>
</LoaderPortal>
)
})
export default TelemetryController

View File

@ -1,16 +1,12 @@
import { Select } from 'antd' import { Select } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useMemo } from 'react'
export const RoleTag = memo(({ roles, value, onChange }) => { export const RoleTag = memo(({ roles, value, onChange }) => {
const [options, setOptions] = useState([]) const options = useMemo(() => roles.map((elm) => ({
key: Date.now(),
useEffect(() => { value: `${elm.caption}`,
setOptions(roles.map((elm) => ({ label: elm.caption
key: Date.now(), })), [roles])
value: `${elm.caption}`,
label: elm.caption
})))
}, [roles])
return ( return (
<Select <Select

View File

@ -1,16 +1,15 @@
import { Button, Input, Tag } from 'antd' import { Button, Input, Tag } from 'antd'
import { UserSwitchOutlined } from '@ant-design/icons' import { UserSwitchOutlined } from '@ant-design/icons'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs' import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { import {
EditableTable, EditableTable,
makeColumn,
makeSelectColumn, makeSelectColumn,
makeActionHandler, makeActionHandler,
makeStringSorter,
makeNumericSorter, makeNumericSorter,
defaultPagination defaultPagination,
makeTextColumn
} from '@components/Table' } from '@components/Table'
import { RoleView } from '@components/views' import { RoleView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
@ -38,7 +37,7 @@ export const UserController = memo(() => {
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
const filteredUsers = users.filter((user) => user && [ const filteredUsers = users.filter((user) => user && (!searchValue || [
user.login ?? '', user.login ?? '',
user.name ?? '', user.name ?? '',
user.surname ?? '', user.surname ?? '',
@ -47,7 +46,7 @@ export const UserController = memo(() => {
user.phone ?? '', user.phone ?? '',
user.position ?? '', user.position ?? '',
user.company?.caption ?? '', user.company?.caption ?? '',
].join(' ').toLowerCase().includes(searchValue)) ].join(' ').toLowerCase().includes(searchValue.toLowerCase())))
setFilteredUsers(filteredUsers) setFilteredUsers(filteredUsers)
}, },
setIsSearching, setIsSearching,
@ -106,8 +105,14 @@ export const UserController = memo(() => {
const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email']) const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email'])
const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))] const roleFilters = [{ text: 'Без роли', value: null }, ...roles.map((role) => ({ text: role.caption, value: role.caption }))]
const rolesRender = (item) => item?.map((elm) => (
<Tag key={elm} color={'blue'}>
<RoleView role={roles.find((role) => role.caption === elm)} />
</Tag>
)) ?? '-'
setColumns([ setColumns([
makeColumn('Логин', 'login', { makeTextColumn('Логин', 'login', null, null, null, {
editable: true, editable: true,
formItemRules: [ formItemRules: [
{ required: true }, { required: true },
@ -121,59 +126,40 @@ export const UserController = memo(() => {
// }) // })
// TODO: Для проверки уникальности логина необходимо исключить из выборки логин выбранного пользователя // TODO: Для проверки уникальности логина необходимо исключить из выборки логин выбранного пользователя
], ],
sorter: makeStringSorter('login'),
}), }),
makeColumn('Фамилия', 'surname', { makeTextColumn('Фамилия', 'surname', filters.surname, null, null, {
editable: true, editable: true,
formItemRules: [{ required: true }, ...nameRules], formItemRules: [{ required: true }, ...nameRules],
sorter: makeStringSorter('surname'),
filters: filters.surname,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('surname'), onFilter: makeTextOnFilter('surname'),
}), }),
makeColumn('Имя', 'name', { makeTextColumn('Имя', 'name', filters.name, null, null, {
editable: true, editable: true,
formItemRules: nameRules, formItemRules: nameRules,
sorter: makeStringSorter('name'),
filters: filters.name,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('name'), onFilter: makeTextOnFilter('name'),
}), }),
makeColumn('Отчество', 'patronymic', { makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, {
editable: true, editable: true,
formItemRules: nameRules, formItemRules: nameRules,
sorter: makeStringSorter('patronymic'),
filters: filters.patronymic,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('patronymic'), onFilter: makeTextOnFilter('patronymic'),
}), }),
makeColumn('E-mail', 'email', { makeTextColumn('E-mail', 'email', filters.email, null, null, {
editable: true, editable: true,
formItemRules: [{ required: true }, ...emailRules], formItemRules: [{ required: true }, ...emailRules],
sorter: makeStringSorter('email'),
filters: filters.email,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('email'), onFilter: makeTextOnFilter('email'),
}), }),
makeColumn('Номер телефона', 'phone', { makeTextColumn('Номер телефона', 'phone', null, null, null, {
editable: true, editable: true,
formItemRules: phoneRules, formItemRules: phoneRules,
sorter: makeStringSorter('phone'),
}), }),
makeColumn('Должность', 'position', { makeTextColumn('Должность', 'position', null, null, null, { editable: true }),
editable: true, makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
sorter: makeStringSorter('position'),
}),
makeColumn('Роли', 'roleNames', {
editable: true, editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
filters: roleFilters,
onFilter: makeArrayOnFilter('roleNames'), onFilter: makeArrayOnFilter('roleNames'),
render: (item) => item?.map((elm) => (
<Tag key={elm} color={'blue'}>
<RoleView role={roles.find((role) => role.caption === elm)} />
</Tag>
)) ?? '-'
}), }),
makeSelectColumn('Компания', 'idCompany', companies, '--', { makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true, editable: true,
@ -186,12 +172,12 @@ export const UserController = memo(() => {
'Получение списка компаний' 'Получение списка компаний'
), []) ), [])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminUserService, service: AdminUserService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable, onComplete: updateTable,
} }), [updateTable])
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject]) const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
@ -200,7 +186,7 @@ export const UserController = memo(() => {
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Input.Search <Input.Search
allowClear allowClear
placeholder={'Поиск пользователей'} placeholder={'Введите текст для поиска (по всем полям за исключением ролей)...'}
onChange={onSearchTextChange} onChange={onSearchTextChange}
style={{ marginBottom: '15px' }} style={{ marginBottom: '15px' }}
loading={isSearching} loading={isSearching}

View File

@ -1,6 +1,6 @@
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table' import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
import { RequestTrackerService } from '@api' import { RequestTrackerService } from '@api'
@ -12,14 +12,22 @@ const columns = [
makeColumn('Логин', 'login', { sorter: makeStringSorter('login') }), makeColumn('Логин', 'login', { sorter: makeStringSorter('login') }),
makeColumn('IP', 'ip', { sorter: makeStringSorter('ip') }), makeColumn('IP', 'ip', { sorter: makeStringSorter('ip') }),
makeColumn('Дата посещения', 'lastDate', { makeColumn('Дата посещения', 'lastDate', {
render: (date) => formatDate(date, false, 'DD MMM YYYY, HH:mm:ss'), render: (date) => formatDate(date, false),
sorter: makeDateSorter('lastDate'), sorter: makeDateSorter('lastDate'),
}), }),
] ]
export const VisitLog = memo(() => { export const VisitLog = memo(() => {
const [logData, setLogData] = useState([]) const [logData, setLogData] = useState([])
const [isLoading, setIsLoading] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
const filteredLogData = useMemo(() => logData.filter((data) => data && (!searchValue || [
data.login ?? '',
data.ip ?? '',
data.lastDate ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [logData, searchValue])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -27,23 +35,30 @@ export const VisitLog = memo(() => {
logData.forEach((log) => log.key = `${log.login}${log.ip}`) logData.forEach((log) => log.key = `${log.login}${log.ip}`)
setLogData(logData) setLogData(logData)
}, },
setIsLoading, setShowLoader,
`Не удалось загрузить список последних посещений пользователей`, `Не удалось загрузить список последних посещений пользователей`,
'Получение списка последних посещений' 'Получение списка последних посещений'
), []) ), [])
return ( return (
<LoaderPortal show={isLoading}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<Table <Table
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={logData} loading={showLoader}
dataSource={filteredLogData}
pagination={defaultPagination} pagination={defaultPagination}
tableName={'visit_log'} tableName={'visit_log'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,23 +0,0 @@
import { memo } from 'react'
import { Select } from 'antd'
import { getTelemetryLabel } from '@components/views'
export const TelemetrySelect = memo(({ telemetry, value, onChange }) => (
<Select
allowClear
value={value?.id}
onChange={(id) => onChange?.(telemetry.find((row) => row.id === id))}
dropdownClassName={'telemetry_select'}
>
{telemetry.map((row, i) => (
<Select.Option key={i} value={row.id}>
<span className={row?.info?.well ? 'telemetry_used' : 'telemetry_unused'}>
{getTelemetryLabel(row)}
</span>
</Select.Option>
))}
</Select>
))
export default TelemetrySelect

View File

@ -1,6 +1,6 @@
import { Button } from 'antd' import { Button, Input } from 'antd'
import { CopyOutlined } from '@ant-design/icons' import { CopyOutlined } from '@ant-design/icons'
import { memo, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { import {
AdminClusterService, AdminClusterService,
@ -19,16 +19,13 @@ import {
defaultPagination, defaultPagination,
makeTimezoneColumn, makeTimezoneColumn,
} from '@components/Table' } from '@components/Table'
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 TelemetrySelect from '@components/selectors/TelemetrySelect'
import { hasPermission } from '@utils/permissions' 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 '@styles/admin.css'
const wellTypes = [ const wellTypes = [
{ value: 1, label: 'Наклонно-направленная' }, { value: 1, label: 'Наклонно-направленная' },
@ -44,8 +41,17 @@ 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)
const [searchValue, setSearchValue] = useState('')
const updateTable = async () => invokeWebApiWrapperAsync( const filteredWells = useMemo(() => wells.filter((well) => well && (!searchValue || [
well.caption ?? '',
well.latitude?.toString() ?? '',
well.longitude?.toString() ?? '',
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [wells, searchValue])
const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
async () => { async () => {
const wells = await AdminWellService.getAll() const wells = await AdminWellService.getAll()
setWells(arrayOrDefault(wells)) setWells(arrayOrDefault(wells))
@ -53,11 +59,11 @@ export const WellController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список скважин`, `Не удалось загрузить список скважин`,
'Получение списка скважин' 'Получение списка скважин'
) ), [])
const duplicateWell = (well) => { const duplicateWell = useCallback((well) => {
// TODO: Метод дубликации скважины // TODO: Метод дубликации скважины
} }, [])
const addititonalButtons = memo((record, editingKey) => ( const addititonalButtons = memo((record, editingKey) => (
<Button <Button
@ -112,22 +118,30 @@ export const WellController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список кустов`, `Не удалось загрузить список кустов`,
'Получение списка кустов' 'Получение списка кустов'
), []) ), [updateTable])
const handlerProps = { const handlerProps = useMemo(() => ({
service: AdminWellService, service: AdminWellService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable onComplete: updateTable
} }), [updateTable])
return ( return (
<LoaderPortal show={showLoader}> <>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={wells} loading={showLoader}
dataSource={filteredWells}
pagination={defaultPagination} pagination={defaultPagination}
onRowAdd={hasPermission('AdminWell.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление скважины')} onRowAdd={hasPermission('AdminWell.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление скважины')}
onRowEdit={hasPermission('AdminWell.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование скважины')} onRowEdit={hasPermission('AdminWell.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование скважины')}
@ -136,7 +150,7 @@ export const WellController = memo(() => {
buttonsWidth={95} buttonsWidth={95}
tableName={'admin_well_controller'} tableName={'admin_well_controller'}
/> />
</LoaderPortal> </>
) )
}) })

View File

@ -1,5 +1,5 @@
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { lazy, Suspense } from 'react' import { lazy, memo, Suspense } from 'react'
import { Switch, useParams } from 'react-router-dom' import { Switch, useParams } from 'react-router-dom'
import { PrivateMenuItem, PrivateRoute, PrivateDefaultRoute } from '@components/Private' import { PrivateMenuItem, PrivateRoute, PrivateDefaultRoute } from '@components/Private'
@ -14,12 +14,12 @@ const WellController = lazy(() => import( './WellController'))
const RoleController = lazy(() => import( './RoleController')) const RoleController = lazy(() => import( './RoleController'))
const CompanyTypeController = lazy(() => import('./CompanyTypeController')) const CompanyTypeController = lazy(() => import('./CompanyTypeController'))
const PermissionController = lazy(() => import( './PermissionController')) const PermissionController = lazy(() => import( './PermissionController'))
const TelemetryController = lazy(() => import( './TelemetryController')) const TelemetrySection = lazy(() => import( './Telemetry'))
const VisitLog = lazy(() => import( './VisitLog')) const VisitLog = lazy(() => import( './VisitLog'))
const rootPath = '/admin' const rootPath = '/admin'
export const AdminPanel = () => { export const AdminPanel = memo(() => {
const { tab } = useParams() const { tab } = useParams()
return ( return (
@ -33,7 +33,7 @@ export const AdminPanel = () => {
<PrivateMenuItem.Link root={rootPath} key={'company_type'} path={'company_type'} title={'Типы компаний' } /> <PrivateMenuItem.Link root={rootPath} key={'company_type'} path={'company_type'} title={'Типы компаний' } />
<PrivateMenuItem.Link root={rootPath} key={'role' } path={'role' } title={'Роли' } /> <PrivateMenuItem.Link root={rootPath} key={'role' } path={'role' } title={'Роли' } />
<PrivateMenuItem.Link root={rootPath} key={'permission' } path={'permission' } title={'Разрешения' } /> <PrivateMenuItem.Link root={rootPath} key={'permission' } path={'permission' } title={'Разрешения' } />
<PrivateMenuItem.Link root={rootPath} key={'telemetry' } path={'telemetry' } title={'Телеметрии' } /> <PrivateMenuItem.Link root={rootPath} key={'telemetry' } path={'telemetry' } title={'Телеметрия' } />
<PrivateMenuItem.Link root={rootPath} key={'visit_log' } path={'visit_log' } title={'Журнал посещений'} /> <PrivateMenuItem.Link root={rootPath} key={'visit_log' } path={'visit_log' } title={'Журнал посещений'} />
</Menu> </Menu>
@ -41,16 +41,16 @@ export const AdminPanel = () => {
<Layout.Content className={'site-layout-background'}> <Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}> <Suspense fallback={<SuspenseFallback />}>
<Switch> <Switch>
<PrivateRoute path={`${rootPath}/deposit` } component={ DepositController} /> <PrivateRoute path={`${rootPath}/deposit` } component={ DepositController} />
<PrivateRoute path={`${rootPath}/cluster` } component={ ClusterController} /> <PrivateRoute path={`${rootPath}/cluster` } component={ ClusterController} />
<PrivateRoute path={`${rootPath}/well` } component={ WellController} /> <PrivateRoute path={`${rootPath}/well` } component={ WellController} />
<PrivateRoute path={`${rootPath}/user` } component={ UserController} /> <PrivateRoute path={`${rootPath}/user` } component={ UserController} />
<PrivateRoute path={`${rootPath}/company` } component={ CompanyController} /> <PrivateRoute path={`${rootPath}/company` } component={ CompanyController} />
<PrivateRoute path={`${rootPath}/company_type`} component={CompanyTypeController} /> <PrivateRoute path={`${rootPath}/company_type` } component={CompanyTypeController} />
<PrivateRoute path={`${rootPath}/role` } component={ RoleController} /> <PrivateRoute path={`${rootPath}/role` } component={ RoleController} />
<PrivateRoute path={`${rootPath}/permission` } component={ PermissionController} /> <PrivateRoute path={`${rootPath}/permission` } component={ PermissionController} />
<PrivateRoute path={`${rootPath}/telemetry` } component={ TelemetryController} /> <PrivateRoute path={`${rootPath}/telemetry/:tab?`} component={TelemetrySection} />
<PrivateRoute path={`${rootPath}/visit_log` } component={VisitLog} /> <PrivateRoute path={`${rootPath}/visit_log` } component={VisitLog} />
<PrivateDefaultRoute urls={[ <PrivateDefaultRoute urls={[
`${rootPath}/deposit`, `${rootPath}/deposit`,
`${rootPath}/cluster`, `${rootPath}/cluster`,
@ -69,6 +69,6 @@ export const AdminPanel = () => {
</Layout> </Layout>
</Layout> </Layout>
) )
} })
export default AdminPanel export default AdminPanel

View File

@ -2,8 +2,8 @@ import { Table as RawTable, Typography } from 'antd'
import { Fragment, memo, useCallback, useEffect, useState } from 'react' import { Fragment, memo, useCallback, useEffect, useState } from 'react'
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 { WellSelector } from '@components/selectors/WellSelector'
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { OperationStatService, WellOperationService } from '@api' import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -18,10 +18,17 @@ const { Cell, Row } = Summary
const numericRender = makeNumericRender() const numericRender = makeNumericRender()
const speedNumericRender = (section) => numericRender(section?.speed) const speedNumericRender = (section) => numericRender(section?.speed)
const makeSectionSorter = (key, name) => (a, b) => (a?.[key]?.[name] ?? 0) - (b?.[key]?.[name] ?? 0)
export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [ export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [
makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100), makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100, {
makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100), sorter: makeSectionSorter(key, 'depth'),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100), }),
makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100, {
sorter: makeSectionSorter(key, 'time'),
}),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100, {
sorter: makeSectionSorter(key, 'speed'),
}),
]) ])
export const defaultColumns = [ export const defaultColumns = [
@ -90,21 +97,30 @@ export const Statistics = memo(({ idWell }) => {
async () => { async () => {
const types = await WellOperationService.getSectionTypes(idWell) const types = await WellOperationService.getSectionTypes(idWell)
setSectionTypes(Object.entries(types)) setSectionTypes(Object.entries(types))
},
setIsPageLoading,
`Не удалось получить типы секции`,
`Получение списка возможных секций`,
), [idWell])
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const filteredSections = avgData?.length > 0 ? sectionTypes.filter(([id, _]) => avgData.some((row) => `section_${id}` in row)) : sectionTypes
setAvgColumns([ setAvgColumns([
...defaultColumns, ...defaultColumns,
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`)), ...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`)),
]) ])
setCmpColumns([ setCmpColumns([
...defaultColumns, ...defaultColumns,
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`, { ...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`, {
speedRender: cmpSpeedRender(`section_${id}`) speedRender: cmpSpeedRender(`section_${id}`)
})) }))
]) ])
}, },
setIsPageLoading, setIsPageLoading,
`Не удалось получить типы секции`, 'Не удалось установить необходимые столбцы'
`Получение списка возможных секций`, ), [sectionTypes, avgData, cmpSpeedRender])
), [idWell, cmpSpeedRender])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,5 +1,5 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useCallback } from 'react' import { useState, useEffect, memo, useCallback, useMemo } from 'react'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd' import { Table, Tag, Button, Badge, Divider, Modal, Row, Col, Popconfirm } from 'antd'
@ -29,9 +29,7 @@ const filtersSectionsType = []
const DAY_IN_MS = 1000 * 60 * 60 * 24 const DAY_IN_MS = 1000 * 60 * 60 * 24
export const WellCompositeSections = memo(({ idWell, statsWells, selectedSections }) => { export const WellCompositeSections = memo(({ idWell, statsWells, selectedSections }) => {
const [rows, setRows] = useState([])
const [params, setParams] = useState([]) const [params, setParams] = useState([])
const [paramsColumns, setParamsColumns] = useState([])
const [selectedWells, setSelectedWells] = useState([]) const [selectedWells, setSelectedWells] = useState([])
const [wellOperations, setWellOperations] = useState([]) const [wellOperations, setWellOperations] = useState([])
const [selectedWellsKeys, setSelectedWellsKeys] = useState([]) const [selectedWellsKeys, setSelectedWellsKeys] = useState([])
@ -44,21 +42,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const location = useLocation() const location = useLocation()
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell]) const paramsColumns = useMemo(async() => await getColumns(idWell), [idWell])
useEffect(() => { const rows = useMemo(() => {
if (isOpsModalVisible || selectedWellId <= 0) return
invokeWebApiWrapperAsync(
async () => {
const { operations } = await getOperations(selectedWellId)
setWellOperations(operations)
},
setShowLoader,
`Не удалось загрузить операции по скважине "${selectedWellId}"`,
)
}, [selectedWellId, isOpsModalVisible])
useEffect(() => {
const rows = [] const rows = []
statsWells?.forEach((well) => { statsWells?.forEach((well) => {
well.sections?.forEach((section) => { well.sections?.forEach((section) => {
@ -113,9 +99,21 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
'nonProductiveTimeFact', 'nonProductiveTimeFact',
]) ])
setRows(rows) return rows
}, [statsWells]) }, [statsWells])
useEffect(() => {
if (isOpsModalVisible || selectedWellId <= 0) return
invokeWebApiWrapperAsync(
async () => {
const { operations } = await getOperations(selectedWellId)
setWellOperations(operations)
},
setShowLoader,
`Не удалось загрузить операции по скважине "${selectedWellId}"`,
)
}, [selectedWellId, isOpsModalVisible])
useEffect(() => { useEffect(() => {
const selected = rows.filter((row) => selectedSections.some(section => ( const selected = rows.filter((row) => selectedSections.some(section => (
section.idWellSrc === row.id && section.idWellSectionType === row.sectionId section.idWellSrc === row.id && section.idWellSectionType === row.sectionId
@ -125,7 +123,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
setSelectedWellsKeys(selected.map((row) => row.key)) setSelectedWellsKeys(selected.map((row) => row.key))
}, [rows, selectedSections]) }, [rows, selectedSections])
const columns = [ const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link> (text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
), ),
@ -171,9 +169,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
</Tag> </Tag>
)) ?? '-', )) ?? '-',
}, },
] ], [location.pathname])
const rowSelection = hasPermission('WellOperation.edit') && { const rowSelection = useMemo(() => hasPermission('WellOperation.edit') && {
selectedRowKeys: selectedWellsKeys, selectedRowKeys: selectedWellsKeys,
onChange: (keys, items) => invokeWebApiWrapperAsync( onChange: (keys, items) => invokeWebApiWrapperAsync(
async () => { async () => {
@ -187,7 +185,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
`Не удалось сохранить изменения выбранных секций для композитной скважины "${idWell}"`, `Не удалось сохранить изменения выбранных секций для композитной скважины "${idWell}"`,
'Изменение выбранных секций скважины' 'Изменение выбранных секций скважины'
) )
} }, [idWell, selectedWellsKeys])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync( const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -8,7 +8,7 @@ import {
} from '@api' } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import WellSelector from '@components/WellSelector' import WellSelector from '@components/selectors/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'

View File

@ -1,4 +1,4 @@
import { memo } from 'react' import { memo, useMemo } from 'react'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { Switch, useParams } from 'react-router-dom' import { Switch, useParams } from 'react-router-dom'
@ -9,7 +9,7 @@ import Statistics from './Statistics'
export const Analytics = memo(({ idWell }) => { export const Analytics = memo(({ idWell }) => {
const { tab } = useParams() const { tab } = useParams()
const rootPath = `/well/${idWell}/analytics` const rootPath = useMemo(() => `/well/${idWell}/analytics`, [idWell])
return ( return (
<Layout> <Layout>

View File

@ -1,21 +1,14 @@
import { useEffect, useState } from 'react' import { memo, useMemo } from 'react'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import { Column } from '@components/charts/Column' import { Column } from '@components/charts/Column'
export const ArchiveColumn = ({ lineGroup, data, interval, style, headerHeight, yStart }) => { export const ArchiveColumn = memo(({ lineGroup, data, interval, style, headerHeight, yStart }) => {
const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([]) const lgws = useMemo(() => lineGroup.filter(cfg => !cfg.isShape), [lineGroup])
const [pv, setPV] = useState([]) const pv = useMemo(() => lgws.filter(line => line.showLabels).map(line => ({
color: line.color,
useEffect(() => { label: line.label
const lgws = lineGroup.filter(cfg => !cfg.isShape) })), [lgws])
setLineGroupWithoutShapes(lgws)
setPV(lgws.filter(line => line.showLabels).map(line => ({
color: line.color,
label: line.label
})))
}, [lineGroup])
return ( return (
<div style={style}> <div style={style}>
@ -26,13 +19,13 @@ export const ArchiveColumn = ({ lineGroup, data, interval, style, headerHeight,
</Grid> </Grid>
<Column <Column
data={data} data={data}
lineGroup={lineGroupWithoutShapes} lineGroup={lgws}
interval={interval} interval={interval}
yDisplay={false} yDisplay={false}
yStart={yStart} yStart={yStart}
/> />
</div> </div>
) )
} })
export default ArchiveColumn export default ArchiveColumn

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { memo, useMemo } from 'react'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
@ -41,13 +41,9 @@ export const cutData = (data, beginDate, endDate) => {
return data return data
} }
export const ArchiveDisplay = ({data, startDate, interval, onWheel}) => { export const ArchiveDisplay = memo(({data, startDate, interval, onWheel}) => {
const [chartData, setChartData] = useState([]) const endDate = useMemo(() => new Date(+startDate + interval), [startDate, interval])
const chartData = useMemo(() => cutData(data, startDate, endDate), [data, startDate, endDate])
useEffect(() => {
const endDate = new Date(+startDate + interval)
setChartData(cutData(data, startDate, endDate))
}, [data, startDate, interval])
return ( return (
<Grid onWheel={onWheel}> <Grid onWheel={onWheel}>
@ -65,6 +61,6 @@ export const ArchiveDisplay = ({data, startDate, interval, onWheel}) => {
))} ))}
</Grid> </Grid>
) )
} })
export default ArchiveDisplay export default ArchiveDisplay

View File

@ -1,11 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { Flex } from '@components/Grid' import { Flex } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { TelemetryDataSaubService } from '@api' import { TelemetryDataSaubService } from '@api'
import { normalizeData } from '@pages/TelemetryView' import { normalizeData } from '@pages/TelemetryView'
@ -69,7 +69,7 @@ export const Archive = memo(({ idWell }) => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [loaded, setLoaded] = useState(null) const [loaded, setLoaded] = useState(null)
const onGraphWheel = (e) => { const onGraphWheel = useCallback((e) => {
if (loaded && dateLimit.from && dateLimit.to) { if (loaded && dateLimit.from && dateLimit.to) {
setStartDate((prevStartDate) => { setStartDate((prevStartDate) => {
const offset = e.deltaY * chartInterval * WHEEL_SENSITIVITY const offset = e.deltaY * chartInterval * WHEEL_SENSITIVITY
@ -79,14 +79,15 @@ export const Archive = memo(({ idWell }) => {
return new Date(Math.max(firstPossibleDate, Math.min(nextStartDate, lastPossibleDate))) return new Date(Math.max(firstPossibleDate, Math.min(nextStartDate, lastPossibleDate)))
}) })
} }
} }, [loaded, dateLimit, chartInterval])
const isDateDisabled = (date) => { const isDateDisabled = useCallback((date) => {
if (!date) return false if (!date) return false
const dt = new Date(date).setHours(0, 0, 0, 0) const dt = new Date(date).setHours(0, 0, 0, 0)
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
} }, [dateLimit])
const isDateTimeDisabled = (date) => ({
const isDateTimeDisabled = useCallback((date) => ({
disabledHours: () => range(0, 24).filter(h => { disabledHours: () => range(0, 24).filter(h => {
if (!date) return false if (!date) return false
const dt = +new Date(date).setHours(h) const dt = +new Date(date).setHours(h)
@ -102,7 +103,7 @@ export const Archive = memo(({ idWell }) => {
const dt = +new Date(date).setSeconds(s) const dt = +new Date(date).setSeconds(s)
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
}) })
}) }), [dateLimit])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,5 +1,5 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useMemo } 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'
@ -108,7 +108,7 @@ export const ClusterWells = memo(({ statsWells }) => {
setTableData(data) setTableData(data)
}, [statsWells]) }, [statsWells])
const columns = [ const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(_, item) => ( (_, item) => (
<Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}> <Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}>
@ -159,7 +159,7 @@ export const ClusterWells = memo(({ statsWells }) => {
</Tag> </Tag>
)) ?? '-', )) ?? '-',
}, },
] ], [location.pathname])
return ( return (
<> <>

View File

@ -1,18 +1,19 @@
import { memo, useMemo } from 'react'
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 '@utils/functions'
export const WellOperationsTable = ({ wellOperations }) => { const columns = [
const columns = [ makeTextColumn('Конструкция секции', 'sectionType'),
makeTextColumn('Конструкция секции','sectionType'), makeTextColumn('Операция', 'operationName'),
makeTextColumn('Операция','operationName'), makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, getPrecision),
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, getPrecision), makeNumericColumnPlanFact('Часы', 'durationHours', null, null, getPrecision),
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, getPrecision), makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-')
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-') ]
]
const operations = wellOperations?.map(el => ({ export const WellOperationsTable = memo(({ wellOperations }) => {
const operations = useMemo(() => wellOperations?.map(el => ({
key: el.plan?.id ?? el.fact.id, key: el.plan?.id ?? el.fact.id,
sectionType: el.plan?.wellSectionTypeName ?? el.fact?.wellSectionTypeName, sectionType: el.plan?.wellSectionTypeName ?? el.fact?.wellSectionTypeName,
operationName: `${el.plan?.categoryName ?? el.fact?.categoryName ?? ''} ${' '} operationName: `${el.plan?.categoryName ?? el.fact?.categoryName ?? ''} ${' '}
@ -23,7 +24,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
durationHoursFact: el.fact?.durationHours, durationHoursFact: el.fact?.durationHours,
commentPlan: el.plan?.comment ?? '-', commentPlan: el.plan?.comment ?? '-',
commentFact: el.fact?.comment ?? '-' commentFact: el.fact?.comment ?? '-'
})) })), [wellOperations])
return ( return (
<Table <Table
@ -36,6 +37,6 @@ export const WellOperationsTable = ({ wellOperations }) => {
tableName={'well_operations'} tableName={'well_operations'}
/> />
) )
} })
export default WellOperationsTable export default WellOperationsTable

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, memo } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import ClusterWells from './ClusterWells' import ClusterWells from './ClusterWells'
export const Cluster = () => { export const Cluster = memo(() => {
const { idCluster } = useParams() const { idCluster } = useParams()
const [data, setData] = useState([]) const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -28,6 +28,6 @@ export const Cluster = () => {
<ClusterWells statsWells={data} /> <ClusterWells statsWells={data} />
</LoaderPortal> </LoaderPortal>
) )
} })
export default Cluster export default Cluster

View File

@ -1,14 +1,13 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useMemo, useCallback } from 'react'
import { DatePicker, Button, Input } from 'antd' import { DatePicker, Button, Input } from 'antd'
import { FileService } from '@api' import { FileService } from '@api'
import { hasPermission } from '@utils/permissions'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { UploadForm } from '@components/UploadForm' import { UploadForm } from '@components/UploadForm'
import { CompanyView, UserView } from '@components/views' import { CompanyView, UserView } from '@components/views'
import { EditableTable, makePaginationObject } from '@components/Table' import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
import { formatDate } from '@utils' import { hasPermission } from '@utils/permissions'
const pageSize = 12 const pageSize = 12
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
@ -24,26 +23,11 @@ const columns = [
{name} {name}
</Button> </Button>
), ),
}, { },
title: 'Дата загрузки', makeDateColumn('Дата загрузки', 'uploadDate'),
key: 'uploadDate', makeNumericColumn('Размер', 'size', null, null, formatBytes),
dataIndex: 'uploadDate', makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
render: item => formatDate(item, false, 'DD MMM YYYY, HH:mm:ss'), makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> })
}, {
title: 'Размер',
key: 'size',
dataIndex: 'size',
render: item => formatBytes(item)
}, {
title: 'Автор',
key: 'author',
dataIndex: 'author',
render: item => <UserView user={item}/>
}, {
title: 'Компания',
key: 'company',
render: (_, record) => <CompanyView company={record?.author?.company}/>
}
] ]
export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange, tableName }) => { export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange, tableName }) => {
@ -55,21 +39,13 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const uploadUrl = `/api/well/${idWell}/files/?idCategory=${idCategory}` const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory])
const handleUploadComplete = () => update() const mergedColumns = useMemo(() => [...columns, ...(customColumns ?? [])], [customColumns])
const companies = useMemo(() => [...new Set(files.map(file => file.company))].filter(company => company), [files])
const filenames = useMemo(() => [...new Set(files.map(file => file.name))].filter(name => name), [files])
const handleFileDelete = async (file) => { const update = useCallback(() => {
await FileService.delete(idWell, file.id)
update()
}
const hanleCompanySearch = (value, _) => setFilterCompanyName(value)
const hanleFileNameSearch = (value, _) => setFilterFileName(value)
const mergedColumns = [...columns, ...(customColumns ?? [])]
const update = () => {
let begin = null let begin = null
let end = null let end = null
if (filterDataRange?.length > 1) { if (filterDataRange?.length > 1) {
@ -101,13 +77,15 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
`Не удалось загрузить файлы по скважине "${idWell}"`, `Не удалось загрузить файлы по скважине "${idWell}"`,
'Загрузка файла по скважине' 'Загрузка файла по скважине'
) )
} }, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page])
useEffect(update, [idWell, idCategory, page, filterDataRange, filterCompanyName, filterFileName]) useEffect(update, [update])
useEffect(() => onChange?.(files), [files, onChange]) useEffect(() => onChange?.(files), [files, onChange])
const companies = [...new Set(files.map(file => file.company))].filter(company => company) const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => {
const filenames = [...new Set(files.map(file => file.name))].filter(name => name) await FileService.delete(idWell, file.id)
update()
}), [idWell, idCategory, update])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -124,7 +102,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
<Search <Search
list={'listCompanies'} list={'listCompanies'}
placeholder={'Фильтр по компании'} placeholder={'Фильтр по компании'}
onSearch={hanleCompanySearch} onSearch={setFilterCompanyName}
/> />
<datalist id={'listCompanies'}> <datalist id={'listCompanies'}>
{companies.map((company) => ( {companies.map((company) => (
@ -138,7 +116,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
<Search <Search
list={'listFileNames'} list={'listFileNames'}
placeholder={'Фильтр по имени файла'} placeholder={'Фильтр по имени файла'}
onSearch={hanleFileNameSearch} onSearch={setFilterFileName}
/> />
<datalist id={'listFileNames'}> <datalist id={'listFileNames'}>
{filenames.map((name) => ( {filenames.map((name) => (
@ -154,7 +132,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
url={uploadUrl} url={uploadUrl}
accept={accept} accept={accept}
onUploadStart={() => setShowLoader(true)} onUploadStart={() => setShowLoader(true)}
onUploadComplete={handleUploadComplete} onUploadComplete={update}
/> />
</div> </div>
)} )}
@ -169,9 +147,9 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
pagination={{ pagination={{
...pagination, ...pagination,
showSizeChanger: false, showSizeChanger: false,
onChange: (page) => setPage(page), onChange: setPage,
}} }}
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete} onRowDelete={handleFileDelete}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={tableName ?? `file_${idCategory}`} tableName={tableName ?? `file_${idCategory}`}
/> />

View File

@ -1,5 +1,5 @@
import { join } from 'path' import { join } from 'path'
import { memo } from 'react' import { memo, useMemo } from 'react'
import { Layout, Menu } from 'antd' import { Layout, Menu } from 'antd'
import { FolderOutlined } from '@ant-design/icons' import { FolderOutlined } from '@ant-design/icons'
import { Switch, useParams } from 'react-router-dom' import { Switch, useParams } from 'react-router-dom'
@ -25,7 +25,7 @@ export const documentCategories = [
export const MenuDocuments = memo(({ idWell }) => { export const MenuDocuments = memo(({ idWell }) => {
const { category } = useParams() const { category } = useParams()
const root = `/well/${idWell}/document` const root = useMemo(() => `/well/${idWell}/document`, [idWell])
return ( return (
<> <>

View File

@ -2,7 +2,7 @@ import { Form, Select } from 'antd'
import { FileAddOutlined } from '@ant-design/icons' import { FileAddOutlined } from '@ant-design/icons'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import Poprompt from '@components/Poprompt' import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'

View File

@ -1,5 +1,5 @@
import { Input, Modal, Radio } from 'antd' import { Input, Modal, Radio } from 'antd'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs' import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { UserView } from '@components/views' import { UserView } from '@components/views'
@ -113,7 +113,7 @@ export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) =>
`Изменение статуса пользователя` `Изменение статуса пользователя`
), [users, idWell, category.idFileCategory]) ), [users, idWell, category.idFileCategory])
const userColumns = [ const userColumns = useMemo(() => [
makeColumn('Пользователь', 'user', { makeColumn('Пользователь', 'user', {
sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0, sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0,
render: (user) => <UserView user={user} />, render: (user) => <UserView user={user} />,
@ -129,7 +129,7 @@ export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) =>
/> />
), ),
}) })
] ], [changeUserStatus])
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject]) const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])

View File

@ -1,4 +1,4 @@
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Button, Input, Popconfirm, Form } from 'antd' import { Button, Input, Popconfirm, Form } from 'antd'
import { import {
DeleteOutlined, DeleteOutlined,
@ -6,11 +6,11 @@ import {
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import Poprompt from '@components/Poprompt'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import UploadForm from '@components/UploadForm' import UploadForm from '@components/UploadForm'
import DownloadLink from '@components/DownloadLink' import DownloadLink from '@components/DownloadLink'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import Poprompt from '@components/selectors/Poprompt'
import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory' import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
import { formatDate } from '@utils' import { formatDate } from '@utils'
@ -44,7 +44,7 @@ export const CategoryRender = memo(({ idWell, partData, onUpdate, onEdit, onHist
file // Информация о файле file // Информация о файле
} = partData ?? {} } = partData ?? {}
const uploadUrl = `/api/well/${idWell}/drillingProgram/part/${idFileCategory}` const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory])
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)

View File

@ -8,7 +8,7 @@ import {
ReloadOutlined, ReloadOutlined,
WarningOutlined, WarningOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { memo, useCallback, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory' import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory'
@ -52,11 +52,11 @@ export const DrillingProgram = memo(({ idWell }) => {
parts, parts,
program, program,
error, error,
} = data } = useMemo(() => data, [data])
const stateId = idState ?? idStateUnknown const stateId = useMemo(() => idState ?? idStateUnknown, [idState])
const state = stateString[stateId] const state = useMemo(() => stateString[stateId], [stateId])
const StateIcon = state.icon const StateIcon = useMemo(() => state.icon, [state?.icon])
const updateData = useCallback(async () => await invokeWebApiWrapperAsync( const updateData = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
@ -76,15 +76,15 @@ export const DrillingProgram = memo(({ idWell }) => {
useEffect(() => updateData(), [updateData]) useEffect(() => updateData(), [updateData])
const onCategoryEdit = (catId) => { const onCategoryEdit = useCallback((catId) => {
setSelectedCategory(catId) setSelectedCategory(catId)
setEditorVisible(!!catId) setEditorVisible(!!catId)
} }, [])
const onCategoryHistory = (catId) => { const onCategoryHistory = useCallback((catId) => {
setSelectedCategory(catId) setSelectedCategory(catId)
setHistoryVisible(!!catId) setHistoryVisible(!!catId)
} }, [])
const onEditorClosed = useCallback(() => { const onEditorClosed = useCallback(() => {
setEditorVisible(false) setEditorVisible(false)

View File

@ -13,9 +13,10 @@ import '@styles/index.css'
import Logo from '@images/Logo' import Logo from '@images/Logo'
export const Login = memo(() => { export const Login = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const history = useHistory()
const location = useLocation() const location = useLocation()
const [showLoader, setShowLoader] = useState(false)
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync( const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,5 +1,5 @@
import { Modal } from 'antd' import { Modal } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useMemo } from 'react'
import { Table } from '@components/Table' import { Table } from '@components/Table'
import { formatDate } from '@utils' import { formatDate } from '@utils'
@ -18,21 +18,18 @@ const dateColumn = {
} }
export const InclinometryTable = memo(({ group, visible, onClose }) => { export const InclinometryTable = memo(({ group, visible, onClose }) => {
const [tableColumns, setTableColumns] = useState([]) const tableColumns = useMemo(() => [
const [tableData, setTableData] = useState([])
useEffect(() => setTableColumns([
dateColumn, dateColumn,
...(group?.columns?.map((column) => ({ ...(group?.columns?.map((column) => ({
...column, ...column,
title: v(column.title) title: v(column.title)
})) ?? []) })) ?? [])
]), [group?.columns]) ], [group?.columns])
useEffect(() => setTableData(group?.values?.map(row => ({ const tableData = useMemo(() => group?.values?.map(row => ({
date: row.timestamp, date: row.timestamp,
...row.data ...row.data
}))), [group?.values]) })), [group?.values])
return !group?.columns ? null : ( return !group?.columns ? null : (
<Modal <Modal

View File

@ -1,4 +1,4 @@
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { Button, Form, Input, Popconfirm, Timeline } from 'antd' import { Button, Form, Input, Popconfirm, Timeline } from 'antd'
import { import {
CheckSquareOutlined, CheckSquareOutlined,
@ -23,23 +23,20 @@ import '@styles/measure.css'
const createEditingColumns = (cols, renderDelegate) => const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => ({ render: renderDelegate, ...col })) cols.map(col => ({ render: renderDelegate, ...col }))
const disabled = !hasPermission('Measure.edit')
export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additionalButtons }) => { export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additionalButtons }) => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [displayedValues, setDisplayedValues] = useState({}) const [displayedValues, setDisplayedValues] = useState({})
const [editingColumns, setEditingColumns] = useState(group.columns) const [editingColumns, setEditingColumns] = useState(group.columns)
const [isTableEditing, setIsTableEditing] = useState(false) const [isTableEditing, setIsTableEditing] = useState(false)
const [editingActionName, setEditingActionName] = useState('') const [editingActionName, setEditingActionName] = useState('')
const [data, setData] = useState([])
const [measuresForm] = Form.useForm() const [measuresForm] = Form.useForm()
useEffect(() => { const data = useMemo(() => group?.values?.length > 0 ? group.values : [group?.defaultValue], [group?.defaultValue, group?.values])
let data = [group.defaultValue]
if (group?.values?.length > 0) useEffect(() => setDisplayedValues(data.at(-1)), [data])
data = group.values
setData(data)
setDisplayedValues(data.at(-1))
}, [group.defaultValue, group.values])
useEffect(() => { useEffect(() => {
const switchableColumns = createEditingColumns( const switchableColumns = createEditingColumns(
@ -64,17 +61,15 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona
`Не удалось удалить запись ${displayedValues.id} для скважины "${idWell}"`, `Не удалось удалить запись ${displayedValues.id} для скважины "${idWell}"`,
'Удаление записи для скважины' 'Удаление записи для скважины'
) )
const editingDisabled = useMemo(() => disabled || !!displayedValues?.isDefaultData, [displayedValues?.isDefaultData])
const deleteDisabled = useMemo(() => !hasPermission('Measure.delete') || !!displayedValues?.isDefaultData, [displayedValues?.isDefaultData])
const disabled = !hasPermission('Measure.edit') const editTable = useCallback((action) => {
const editingDisabled = disabled || !!displayedValues?.isDefaultData
const deleteDisabled = !hasPermission('Measure.delete') || !!displayedValues?.isDefaultData
const editTable = (action) => {
setEditingActionName(action) setEditingActionName(action)
setIsTableEditing(true) setIsTableEditing(true)
} }, [])
const handleSubmitMeasuresForm = async (formData) => await invokeWebApiWrapperAsync( const handleSubmitMeasuresForm = useCallback(async (formData) => await invokeWebApiWrapperAsync(
async () => { async () => {
measuresForm.validateFields() measuresForm.validateFields()
@ -99,7 +94,7 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona
setShowLoader, setShowLoader,
`Не удалось добавить/изменить запись для скаважины "${idWell}"`, `Не удалось добавить/изменить запись для скаважины "${idWell}"`,
'Добавление/изменение записи по скважине' 'Добавление/изменение записи по скважине'
) ), [displayedValues?.id, displayedValues?.timestamp, editingActionName, group.idCategory, idWell, measuresForm, updateMeasuresFunc])
return ( return (
<> <>

View File

@ -1,5 +1,5 @@
import moment from 'moment' import moment from 'moment'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { Table, Select, DatePicker, Input } from 'antd' import { Table, Select, DatePicker, Input } from 'antd'
import { MessageService } from '@api' import { MessageService } from '@api'
@ -65,6 +65,8 @@ const filterOptions = [
{ value: 3, label: 'Информация' }, { value: 3, label: 'Информация' },
] ]
const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>)
// Данные для таблицы // Данные для таблицы
export const Messages = memo(({ idWell }) => { export const Messages = memo(({ idWell }) => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
@ -75,9 +77,7 @@ export const Messages = memo(({ idWell }) => {
const [searchString, setSearchString] = useState('') const [searchString, setSearchString] = useState('')
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>) const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), [])
const onChangeSearchString = (message) => setSearchString(message.length > 2 ? message : '')
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,6 +1,6 @@
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, useCallback } from 'react'
import { Radio, Button, Select, notification } from 'antd' import { Radio, Button, Select, notification } from 'antd'
import { ReportService } from '@api' import { ReportService } from '@api'
@ -43,7 +43,7 @@ export const Report = memo(({ idWell }) => {
const [pagesCount, setPagesCount] = useState(0) const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const handleReportCreation = async () => await invokeWebApiWrapperAsync( const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
const taskId = await ReportService.createReport( const taskId = await ReportService.createReport(
idWell, idWell,
@ -81,9 +81,11 @@ export const Report = memo(({ idWell }) => {
${filterDateRange[0].format(dateTimeFormat)} по ${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format(dateTimeFormat)}`, ${filterDateRange[1].format(dateTimeFormat)}`,
'Создание отчёта по скважине' 'Создание отчёта по скважине'
) ), [filterDateRange, format, idWell, step])
const disabledDate = (current) => !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]') const disabledDate = useCallback((current) =>
!current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
, [aviableDateRange])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -6,7 +6,7 @@ import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeNumericRender, EditableTable } from '@components/Table' import { makeNumericRender, EditableTable } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
export const SetpointSender = ({ idWell, onClose, visible, setpointNames }) => { export const SetpointSender = ({ idWell, onClose, visible, setpointNames }) => {
const [expirePeriod, setExpirePeriod] = useState(defaultPeriod) const [expirePeriod, setExpirePeriod] = useState(defaultPeriod)

View File

@ -12,7 +12,7 @@ import { makeDateSorter } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid' import { Grid, GridItem, Flex } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'

View File

@ -1,4 +1,4 @@
import { memo } from 'react' import { memo, useMemo } from 'react'
import { import {
FolderOutlined, FolderOutlined,
FundViewOutlined, FundViewOutlined,
@ -30,7 +30,7 @@ const { Content } = Layout
export const Well = memo(() => { export const Well = memo(() => {
const { idWell, tab } = useParams() const { idWell, tab } = useParams()
const rootPath = `/well/${idWell}` const rootPath = useMemo(() => `/well/${idWell}`, [idWell])
return ( return (
<Layout> <Layout>

View File

@ -12,8 +12,8 @@ const style = { margin: 4 }
export const ImportExportBar = memo(({ idWell, onImported, disabled }) => { export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
const [isImportModalVisible, setIsImportModalVisible] = useState(false) const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const downloadTemplate = async () => download(`/api/well/${idWell}/wellOperations/template`) const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`)
const downloadExport = async () => download(`/api/well/${idWell}/wellOperations/export`) const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`)
const onDone = () => { const onDone = () => {
setIsImportModalVisible(false) setIsImportModalVisible(false)

View File

@ -1,183 +0,0 @@
import { Switch } from 'antd'
import { memo, useState, useRef, useEffect } from 'react'
import {
Chart,
TimeScale,
LinearScale,
Legend,
LineController,
PointElement,
LineElement
} from 'chart.js'
import 'chartjs-adapter-moment'
import zoomPlugin from 'chartjs-plugin-zoom'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { getOperations } from '@utils/functions'
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
const scaleTypes = {
day: {
min: 0,
type: 'linear',
display: true,
title: { display: false, text: '' },
ticks: { stepSize: 1 }
},
date: {
display: true,
title: { display: true },
type: 'time',
time: {
unit: 'hour',
displayFormats: { 'hour': 'MM.DD' }
},
grid: { drawTicks: true },
ticks: {
stepSize: 3,
major: { enabled: true },
z: 1,
display: true,
textStrokeColor: '#fff',
textStrokeWidth: 2,
color: '#000',
}
}
}
const defaultOptions = {
responsive: true,
aspectRatio: 2.6,
interaction: {
intersect: false,
mode: 'index',
},
scales: {
x: scaleTypes.day,
y: {
type: 'linear',
position: 'top',
reverse: true,
display: true,
title: {
display: false,
text: '',
}
}
},
parsing: {
xAxisKey: 'day',
yAxisKey: 'depth',
},
elements: {
point: {
radius: 1.7,
//backgroundColor:'#aaa',
//pointStyle:'triangle',
},
},
plugins: {
legend: {
display: true,
},
datalabels: {
display: false,
},
tooltip: {
enabled: true,
callbacks: {
label: (tooltipItem) => tooltipItem.yLabel,
},
position: 'nearest',
},
}
}
const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
label,
data,
backgroundColor: color,
borderColor: color,
borderWidth,
borderDash,
})
export const Tvd = memo(({ idWell, title }) => {
const [operations, setOperations] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [xLabel, setXLabel] = useState('day')
const [chart, setChart] = useState()
const chartRef = useRef(null)
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const operations = await getOperations(idWell)
setOperations(operations)
},
setIsLoading,
`Не удалось загрузить операции по скважине "${idWell}"`,
'Получение списка опервций по скважине'
)
}, [idWell])
useEffect(() => {
const data = {
datasets: [
makeDataset(operations?.fact, 'Факт', '#0A0'),
makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]),
makeDataset(operations?.plan, 'План', '#C004', 4),
]
}
if (chartRef.current && !chart) {
const thisOptions = {}
Object.assign(thisOptions, defaultOptions)
thisOptions.scales.x = scaleTypes[xLabel]
thisOptions.parsing.xAxisKey = xLabel
const newChart = new Chart(chartRef.current, {
type: 'line',
plugins: [ChartDataLabels],
options: thisOptions,
data: data
})
setChart(newChart)
return () => chart?.destroy()
} else {
chart.data = data
chart.options.scales.x = scaleTypes[xLabel]
chart.options.parsing.xAxisKey = xLabel
chart.update()
}
}, [chart, operations, xLabel])
return (
<div className={'container'}>
<div>
{title || (
<h2 className={'mt-20px'}>График Глубина-день</h2>
)}
<LoaderPortal show={isLoading}>
<canvas ref={chartRef} />
<div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
<Switch
checkedChildren={'Дата'}
unCheckedChildren={'Дни со старта'}
loading={isLoading}
onChange={(checked) => setXLabel(checked ? 'date' : 'day')}
/>
</div>
</LoaderPortal>
</div>
</div>
)
})
export default Tvd

View File

@ -0,0 +1,67 @@
import { Descriptions } from 'antd'
import { memo, useEffect, useState } from 'react'
import { makeNumericRender } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime'
import '@styles/tvd.less'
const { Item } = Descriptions
const calcEndDate = (saubData) => {
if (!Array.isArray(saubData) || saubData.length <= 0) return [null, null]
const lastElm = saubData.at(-1)
return [saubData[0]?.date, fractionalSum(lastElm?.date, lastElm?.nptHours, 'hour')]
}
const numericRender = makeNumericRender(2)
const printDate = (date) => formatDate(date) ?? '-'
export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
const [additionalData, setAdditionalData] = useState({})
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const [factStartDate, factEndDate] = calcEndDate(operations.fact)
const [planStartDate, planEndDate] = calcEndDate(operations.plan)
const [predictStartDate, predictEndDate] = calcEndDate(operations.predict)
const last = predictEndDate ?? factEndDate
setAdditionalData({
lag: (+new Date(last) - +new Date(planEndDate)) / 86400_000,
endDate: last,
factStartDate,
factEndDate,
planStartDate,
planEndDate,
predictStartDate,
predictEndDate,
})
},
setIsLoading,
'Не удалось высчитать дополнительные данные'
), [operations, setIsLoading])
return (
<>
<div className={'tvd-tr-table'}>
<Descriptions bordered column={1} size={'small'} style={{ backgroundColor: 'white' }}>
<Item label={'Дата завершения'}>{printDate(additionalData.endDate)}</Item>
<Item label={'Отставание (сут)'}>{numericRender(additionalData.lag)}</Item>
</Descriptions>
</div>
<div className={'tvd-bl-table'} style={{ bottom: xLabel === 'day' ? '35px' : '85px' }}>
<Descriptions bordered column={1} size={'small'} style={{ backgroundColor: 'white' }}>
<Item label={'Отставание (сут)'}>{numericRender(additionalData.lag)}</Item>
<Item label={'Начало цикла (план)'}>{printDate(additionalData.planStartDate)}</Item>
<Item label={'Начало цикла (факт)'}>{printDate(additionalData.factStartDate)}</Item>
<Item label={'Окончание цикла (план)'}>{printDate(additionalData.planEndDate)}</Item>
<Item label={'Окончание цикла (факт)'}>{printDate(additionalData.factEndDate)}</Item>
</Descriptions>
</div>
</>
)
})
export default AdditionalTables

View File

@ -0,0 +1,29 @@
import { memo, useCallback, useState } from 'react'
import { ExportOutlined } from '@ant-design/icons'
import { Button } from 'antd'
import { download, invokeWebApiWrapperAsync } from '@components/factory'
export const NetGraphExport = memo(({ idWell, ...other }) => {
const [isFileExporting, setIsFileExporting] = useState(false)
const onExport = useCallback(() => invokeWebApiWrapperAsync(
async () => await download(`/api/well/${idWell}/wellOperations/scheduleReport`),
setIsFileExporting,
'Не удалось загрузить файл'
), [idWell])
return (
<Button
icon={<ExportOutlined />}
loading={isFileExporting}
onClick={onExport}
style={{ marginRight: '5px' }}
{...other}
>
Сетевой график
</Button>
)
})
export default NetGraphExport

View File

@ -0,0 +1,68 @@
import { memo, useEffect, useState } from 'react'
import { InputNumber } from 'antd'
import { FilterOutlined } from '@ant-design/icons'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeDateColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import '@styles/tvd.less'
export const columns = [
makeTextColumn('Конструкция секции', 'wellSectionTypeName', null, null, null, { width: 140 }),
makeNumericColumn('Глубина', 'depth', null, null, null, 80),
makeDateColumn('Дата начала', 'date', false, undefined, { width: 90 }),
makeNumericColumn('Длительность (ч)', 'durationHours', null, null, makeNumericRender(2), 120),
makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null),
makeTextColumn('Комментарий', 'comment'),
]
export const NptTable = memo(({ operations }) => {
const [filterValue, setFilterValue] = useState(0)
const [npt, setNPT] = useState([])
const [filteredNPT, setFilteredNPT] = useState([])
const [isTableLoading, setIsTableLoading] = useState(false)
useEffect(() => invokeWebApiWrapperAsync(
async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []),
setIsTableLoading,
'Не удалось получить список НПВ'
), [operations])
useEffect(() => invokeWebApiWrapperAsync(
async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)),
setIsTableLoading,
'Не удалось отфильтровать НПВ по времени'
), [npt, filterValue])
return (
<div className={'tvd-right'}>
<div className={'tvd-npt-filter'}>
<FilterOutlined />
<span>Фильтр время НПВ &ge;</span>
<InputNumber
step={0.5}
max={10000}
min={0}
defaultValue={0}
onChange={(value) => setFilterValue(value ?? 0)}
value={filterValue}
/>
<span>ч.</span>
</div>
<LoaderPortal show={isTableLoading}>
<Table
bordered
size={'small'}
dataSource={filteredNPT}
columns={columns}
pagination={false}
tableName={'tvd_npt'}
scroll={{ y: '60vh', scrollToFirstRowOnChange: true }}
/>
</LoaderPortal>
</div>
)
})
export default NptTable

View File

@ -0,0 +1,226 @@
import { useHistory } from 'react-router-dom'
import { memo, useState, useRef, useEffect, useCallback } from 'react'
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'
import { Switch, Button } from 'antd'
import {
Chart,
TimeScale,
LinearScale,
Legend,
LineController,
PointElement,
LineElement
} from 'chart.js'
import 'chartjs-adapter-moment'
import zoomPlugin from 'chartjs-plugin-zoom'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime'
import { getOperations } from '@utils/functions'
import NptTable from './NptTable'
import NetGraphExport from './NetGraphExport'
import AdditionalTables from './AdditionalTables'
import '@styles/index.css'
import '@styles/tvd.less'
Chart.register(
TimeScale,
LinearScale,
LineController,
LineElement,
PointElement,
Legend,
ChartDataLabels,
zoomPlugin
)
const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-'
const scaleTypes = {
day: {
min: 0,
type: 'linear',
display: true,
title: { display: false, text: '' },
ticks: { stepSize: 1 }
},
date: {
display: true,
title: { display: true },
type: 'time',
time: { unit: 'day', displayFormats: { day: 'DD.MM.YYYY' } },
grid: { drawTicks: true },
ticks: {
stepSize: 3,
major: { enabled: true },
z: 1,
display: true,
textStrokeColor: '#fff',
textStrokeWidth: 2,
color: '#000',
}
}
}
const defaultOptions = {
responsive: true,
maintainAspectRatio: false,
aspectRatio: false,
interaction: { intersect: false, mode: 'point' },
scales: {
x: scaleTypes.day,
y: {
type: 'linear',
position: 'top',
reverse: true,
display: true,
title: { display: false, text: '' }
}
},
parsing: { xAxisKey: 'day', yAxisKey: 'depth' },
elements: { point: { radius: 1.7 } },
plugins: {
legend: { display: true },
datalabels: { display: false },
tooltip: {
enabled: true,
position: 'nearest',
callbacks: {
title: (items) => [
`Дата: ${formatDate(items[0].raw?.date) ?? '-'}`,
`День с начала бурения: ${parseInt(items[0].raw?.day)}`,
],
afterTitle: (items) => `Глубина: ${numericRender(items[0].raw?.depth)}`,
label: (item) => [
item.raw.wellSectionTypeName + ': ' + item.raw.categoryName,
`Длительность (ч): ${item.raw.nptHours}`
],
},
},
},
}
const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
label,
data,
backgroundColor: color,
borderColor: color,
borderWidth,
borderDash,
})
export const Tvd = memo(({ idWell, title }) => {
const [chart, setChart] = useState()
const [xLabel, setXLabel] = useState('day')
const [operations, setOperations] = useState({})
const [tableVisible, setTableVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const chartRef = useRef(null)
const history = useHistory()
const onPointClick = useCallback((e) => {
const points = e?.chart?.tooltip?.dataPoints
if (!points || !(points.length > 0)) return
const datasetId = points.find((p) => p.datasetIndex !== 1)?.datasetIndex
if (typeof datasetId === 'undefined') return
const datasetName = datasetId === 2 ? 'plan' : 'fact'
const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',')
history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
}, [idWell, history])
useEffect(() => invokeWebApiWrapperAsync(
async () => setOperations(await getOperations(idWell)),
setIsLoading,
`Не удалось загрузить операции по скважине "${idWell}"`,
'Получение списка опервций по скважине'
), [idWell])
useEffect(() => {
const withoutNpt = []
operations?.fact?.forEach((row) => {
if (row?.isNPT !== false) return
const nptH = +(row.nptHours ?? 0)
withoutNpt.push({
...row,
day: row.day - nptH / 24,
date: fractionalSum(row.date, -nptH, 'hour'),
})
})
const data = { datasets: [
makeDataset(operations?.fact, 'Факт', '#0A0', 3),
makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]),
makeDataset(operations?.plan, 'План', '#F00', 3),
makeDataset(withoutNpt, 'Факт без НПВ', '#00F', 3)
]}
if (chartRef.current && !chart) {
const thisOptions = {}
Object.assign(thisOptions, defaultOptions)
thisOptions.onClick = onPointClick
thisOptions.scales.x = scaleTypes[xLabel]
thisOptions.parsing.xAxisKey = xLabel
const newChart = new Chart(chartRef.current, {
type: 'line',
plugins: [ChartDataLabels],
options: thisOptions,
data: data
})
setChart(newChart)
return () => chart?.destroy()
} else {
chart.data = data
chart.options.scales.x = scaleTypes[xLabel]
chart.options.parsing.xAxisKey = xLabel
chart.update()
// Обнуление ширины необходимо для уменьшения размена при resize после появления элементов
chart.canvas.parentNode.style.width = '0'
chart.resize()
}
}, [chart, operations, xLabel, onPointClick])
const toogleTable = useCallback(() => {
setOperations(pre => ({ ...pre }))
setTableVisible(v => !v)
}, [])
return (
<div className={'container tvd-page'}>
<div className={'tvd-top'}>
<h2>{title || 'График Глубина-день'}</h2>
<div>
<Switch
checkedChildren={'Дата'}
unCheckedChildren={'Дни со старта'}
loading={isLoading}
onChange={(checked) => setXLabel(checked ? 'date' : 'day')}
style={{ marginRight: '20px' }}
/>
<NetGraphExport idWell={idWell} />
<Button icon={tableVisible ? <DoubleRightOutlined /> : <DoubleLeftOutlined />} onClick={toogleTable}>НПВ</Button>
</div>
</div>
<LoaderPortal show={isLoading} style={{ flex: 1 }}>
<div className={'tvd-main'}>
<div className={'tvd-left'}>
<AdditionalTables operations={operations} xLabel={xLabel} setIsLoading={setIsLoading} />
<canvas ref={chartRef} />
</div>
{tableVisible && <NptTable operations={operations?.fact} />}
</div>
</LoaderPortal>
</div>
)
})
export default Tvd

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, memo } from 'react' import { useState, useEffect, useCallback, memo, useMemo } from 'react'
import { import {
EditableTable, EditableTable,
@ -54,15 +54,15 @@ export const WellDrillParams = memo(({ idWell }) => {
await updateParams() await updateParams()
})(), [idWell, updateParams]) })(), [idWell, updateParams])
const handlerProps = { const handlerProps = useMemo(() => ({
service: DrillParamsService, service: DrillParamsService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateParams, onComplete: updateParams,
idWell idWell
} }), [idWell, updateParams])
const recordParser = (record) => ({ ...record, idWell }) const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

View File

@ -1,86 +1,88 @@
import moment from 'moment' import moment from 'moment'
import { Input } from 'antd' import { Input } from 'antd'
import { useState, useEffect, memo } from 'react' import { useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { import {
EditableTable, EditableTable,
DatePickerWrapper,
makeColumn, makeColumn,
makeDateSorter,
makeNumericColumnOptions, makeNumericColumnOptions,
makeSelectColumn, makeSelectColumn,
makeActionHandler, makeActionHandler,
makeDateColumn,
makeNumericColumn,
makeNumericRender,
makeNumericSorter,
makeTextColumn,
} from '@components/Table' } from '@components/Table'
import { WellOperationService} from '@api'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils/permissions'
import { formatDate } from '@utils' import { arrayOrDefault } from '@utils'
import { WellOperationService } from '@api'
const { TextArea } = Input const { TextArea } = Input
const basePageSize = 160 const basePageSize = 160
const dayRender = makeNumericRender(2)
const dayWithoutNptRender = (_, row) => dayRender((row.day ?? 0) - (row.nptHours ?? 0) / 24)
const defaultColumns = [ const generateColumns = (showNpt = false, categories = [], sectionTypes = []) => [
makeSelectColumn('Конструкция секции', 'idWellSectionType', [], undefined, { makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, {
sorter: makeNumericSorter('idWellSectionType'),
editable: true, editable: true,
width: 160, width: 160,
formItemRules: [({ getFieldValue }) => ({
validator(_, value) {
if (value?.length > 0)
return Promise.resolve()
return Promise.reject('Это обязательное поле!')
}
})],
}), }),
makeSelectColumn('Операция', 'idCategory', [], undefined, { editable: true, width: 200 }), makeSelectColumn('Операция', 'idCategory', categories, undefined, {
makeColumn('Доп. инфо', 'categoryInfo', { editable: true, width: 300, input: <TextArea/> }), sorter: makeNumericSorter('idCategory'),
makeColumn('Глубина забоя на начало, м', 'depthStart', makeNumericColumnOptions(2, 'depthStart')),
makeColumn('Глубина забоя при завершении, м', 'depthEnd', makeNumericColumnOptions(2, 'depthEnd')),
makeColumn('Время начала', 'dateStart', {
editable: true, editable: true,
width: 200, width: 200,
input: <DatePickerWrapper/>,
initialValue: moment().format(),
sorter: makeDateSorter('dateStart'),
render: (_, record) => (
<div className={'text-align-r-container'}>
<span>{formatDate(record.dateStart)}</span>
</div>
)
}), }),
makeColumn('Часы', 'durationHours', makeNumericColumnOptions(2, 'durationHours')), makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null, { editable: true, width: 300, input: <TextArea/> }),
makeColumn('Комментарий', 'comment', { editable: true, input: <TextArea/> }), makeColumn('Глубина забоя на начало, м', 'depthStart', makeNumericColumnOptions(2, 'depthStart')),
] makeColumn('Глубина забоя при завершении, м', 'depthEnd', makeNumericColumnOptions(2, 'depthEnd')),
makeDateColumn('Время начала', 'dateStart', undefined, undefined, {
editable: true,
width: 170,
initialValue: moment().format(),
}),
makeNumericColumn('День', 'day', null, null, dayRender, 80),
showNpt && makeNumericColumn('День без НПВ', 'dayWithoutNpt', null, null, dayWithoutNptRender, 80),
makeColumn('Часы', 'durationHours', { ...makeNumericColumnOptions(2, 'durationHours'), width: 70 }),
makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }),
].filter(Boolean)
export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => { export const WellOperationsEditor = memo(({ idWell, idType, showNpt, ...other }) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({current:1, pageSize:basePageSize}) const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize })
const [paginationTotal, setPaginationTotal] = useState(0) const [paginationTotal, setPaginationTotal] = useState(0)
const [operations, setOperations] = useState([]) const [operations, setOperations] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState(defaultColumns)
const [categories, setCategories] = useState([])
const [sectionTypes, setSectionTypes] = useState([])
const columns = useMemo(() => generateColumns(showNpt, categories, sectionTypes), [showNpt, categories, sectionTypes])
const location = useLocation()
const selectedIds = useMemo(() => {
const query = new URLSearchParams(location.search)
return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt))
}, [location])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
let categories = await WellOperationService.getCategories(idWell) ?? [] const categories = arrayOrDefault(await WellOperationService.getCategories(idWell))
categories = categories.map((item) => ({ value: item.id, label: item.name })) setCategories(categories.map((item) => ({ value: item.id, label: item.name })))
let sectionTypes = await WellOperationService.getSectionTypes(idWell) ?? [] const sectionTypes = Object.entries(await WellOperationService.getSectionTypes(idWell) ?? {})
sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] })) setSectionTypes(sectionTypes.map(([id, label]) => ({ value: parseInt(id), label })))
setColumns(preColumns => {
const newColumns = [...preColumns]
newColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
newColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
return newColumns
})
}, },
setShowLoader, setShowLoader,
'Не удалось загрузить список операций по скважине', 'Не удалось загрузить список операций по скважине',
'Получение списка операций по скважине' 'Получение списка операций по скважине'
), [idWell]) ), [idWell])
const updateOperations = () => invokeWebApiWrapperAsync( const updateOperations = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
const skip = ((pageNumAndPageSize.current - 1) * pageNumAndPageSize.pageSize) || 0 const skip = ((pageNumAndPageSize.current - 1) * pageNumAndPageSize.pageSize) || 0
const take = pageNumAndPageSize.pageSize const take = pageNumAndPageSize.pageSize
@ -95,17 +97,22 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
setShowLoader, setShowLoader,
'Не удалось загрузить список операций по скважине', 'Не удалось загрузить список операций по скважине',
'Получение списка операций по скважине' 'Получение списка операций по скважине'
) ), [idWell, idType, pageNumAndPageSize])
useEffect(updateOperations, [idWell, idType, pageNumAndPageSize]) useEffect(updateOperations, [updateOperations])
const handlerProps = { const handlerProps = useMemo(() => ({
service: WellOperationService, service: WellOperationService,
setLoader: setShowLoader, setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`, errorMsg: `Не удалось выполнить операцию`,
onComplete: updateOperations, onComplete: updateOperations,
idWell idWell
} }), [idWell, updateOperations])
const onRow = useCallback((record) => {
if (selectedIds?.includes(record.id))
return { style: { background: '#BF0000A0' } }
}, [selectedIds])
const recordParser = (record) => ({ const recordParser = (record) => ({
...record, ...record,
@ -133,6 +140,7 @@ export const WellOperationsEditor = memo(({ idWell, idType, ...other }) => {
onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize }) onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
}} }}
tableName={'well_operationse_editor'} tableName={'well_operationse_editor'}
onRow={onRow}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -59,7 +59,7 @@ export const WellOperations = memo(({ idWell }) => {
<WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'}/> <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} showNpt tableName={'well_operations_fact'}/>
</PrivateRoute> </PrivateRoute>
<PrivateRoute path={`${rootPath}/drillProcessFlow`}> <PrivateRoute path={`${rootPath}/drillProcessFlow`}>
<DrillProcessFlow idWell={idWell} /> <DrillProcessFlow idWell={idWell} />

View File

@ -1,12 +0,0 @@
.telemetry_select {
min-width: 300px !important;
}
.telemetry_select .telemetry_used {
color: black;
}
.telemetry_select .telemetry_unused {
color: gray;
}

8
src/styles/charts.less Normal file
View File

@ -0,0 +1,8 @@
.d3-chart {
> .d3-chart-tooltip {
position: absolute;
background: #AAAAAAC0;
padding: 5px;
border-radius: 5px;
}
}

View File

@ -0,0 +1,11 @@
.telemetry_select {
min-width: 300px !important;
& .telemetry_used {
color: black;
}
& .telemetry_unused {
color: gray;
}
}

65
src/styles/tvd.less Normal file
View File

@ -0,0 +1,65 @@
.tvd-page {
flex: 1;
display: flex;
flex-direction: column;
justify-content: stretch;
.tvd-top {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: 20px;
}
.tvd-main {
height: 100%;
display: flex;
overflow: hidden;
align-items: stretch;
flex-flow: row nowrap;
.tvd-left {
position: relative;
flex: 1;
> div {
position: absolute;
//pointer-events: none;
transition: all .25s ease-out;
opacity: 1;
&:hover {
opacity: 0;
}
&.tvd-tr-table {
right: 15px;
top: 38px;
}
&.tvd-bl-table {
bottom: 35px;
left: 50px;
}
}
}
.tvd-right {
flex: 1;
display: flex;
align-items: flex-end;
padding-left: 10px;
flex-direction: column;
justify-content: stretch;
.tvd-npt-filter {
margin-bottom: 10px;
> span {
margin: 0 10px;
}
}
}
}
}

View File

@ -4,7 +4,7 @@ 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 = 'DD.MM.YYYY HH:mm'
export enum timeInS { export enum timeInS {
millisecond = 0.001, millisecond = 0.001,
@ -45,8 +45,8 @@ export const calcDuration = (start: unknown, end: unknown) => {
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 => { export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate | null => {
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return NaN if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return null
const d = new Date(date) const d = new Date(date)
d.setMilliseconds(d.getMilliseconds() + value * timeInS[type] * 1000) d.setMilliseconds(d.getMilliseconds() + value * timeInS[type] * 1000)
return d return d

View File

@ -7,28 +7,37 @@ export const getPrecision = (number: number) => Number.isFinite(number) ? number
export type KeyType = number | string export type KeyType = number | string
export type SaubData = { export const nwtIdCategory = 1043
key?: number
depth?: number export type SaubData = WellOperationDto & {
date?: string isNPT?: boolean // Относится ли операция к НПВ
day?: number key?: number // ID операции
depth?: number // Глубина
date?: string // Дата
nptHours?: number // Колличество часов НПВ с начала бурения до текущего момента
} }
export const getOperations = async (idWell: number): Promise<{ export const getOperations = async (idWell: number): Promise<{
operations?: WellOperationDtoPlanFactPredictBase[], operations: WellOperationDtoPlanFactPredictBase[],
plan?: SaubData[] plan: SaubData[]
fact?: SaubData[] fact: SaubData[]
predict?: SaubData[] predict: SaubData[]
}> => { }> => {
const ops = await OperationStatService.getTvd(idWell) const ops = await OperationStatService.getTvd(idWell)
if (!ops) return {} if (!ops) return {
operations: [],
plan: [],
fact: [],
predict: [],
}
const convert = (operation?: WellOperationDto): SaubData => ({ const convert = (operation?: WellOperationDto): SaubData => ({
...operation,
key: operation?.id, key: operation?.id,
depth: operation?.depthStart, depth: operation?.depthStart,
date: operation?.dateStart, date: operation?.dateStart,
day: operation?.day, isNPT: operation?.idCategory === nwtIdCategory,
}) })
const planData = ops const planData = ops

View File

@ -78,7 +78,10 @@ export const requirements: PermissionRecord = {
permission: ['AdminPermission.get'], permission: ['AdminPermission.get'],
role: ['AdminUserRole.get', 'AdminPermission.get'], role: ['AdminUserRole.get', 'AdminPermission.get'],
visit_log: ['RequerstTracker.get'], visit_log: ['RequerstTracker.get'],
telemetry: ['AdminTelemetry.get'], telemetry: {
merger: ['AdminTelemetry.get'],
viewer: ['AdminTelemetry.get'],
},
}, },
deposit: ['Deposit.get', 'Cluster.get'], deposit: ['Deposit.get', 'Cluster.get'],
cluster: { cluster: {