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 PageHeader from '@components/PageHeader'
import WellTreeSelector from '@components/WellTreeSelector'
import WellTreeSelector from '@components/selectors/WellTreeSelector'
export type LayoutPortalProps = LayoutProps & {
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 { ColumnProps } from 'antd/lib/table'
export { makeDateColumn } from './date'
export {
RegExpIsFloat,
makeNumericRender,

View File

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

View File

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

View File

@ -6,6 +6,7 @@ export {
RegExpIsFloat,
timezoneOptions,
TimezoneSelect,
makeDateColumn,
makeGroupColumn,
makeColumn,
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 { isRawDate } from '@utils'
import LoaderPortal from './LoaderPortal'
import { WellIcon, WellIconState } from './icons'
import { invokeWebApiWrapperAsync } from './factory'
import LoaderPortal from '@components/LoaderPortal'
import { WellIcon, WellIconState } from '@components/icons'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DepositService, DepositDto } from '@api'
import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg'

View File

@ -4,7 +4,7 @@ import { Tooltip } from 'antd'
import { TelemetryDto, TelemetryInfoDto } from '@api'
import { Grid, GridItem } from '@components/Grid'
const lables: Record<string, string> = {
export const lables: Record<string, string> = {
timeZoneId: 'Временная зона',
timeZoneOffsetTotalHours: 'Сдвиг временной зоны',
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 {
EditableTable,
@ -9,7 +10,6 @@ import {
defaultPagination,
makeTimezoneColumn
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils'
@ -22,8 +22,16 @@ export const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([])
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, '--', {
width: 200,
editable: true,
@ -38,7 +46,7 @@ export const ClusterController = memo(() => {
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
]
], [deposits])
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => {
@ -63,27 +71,35 @@ export const ClusterController = memo(() => {
useEffect(updateTable, [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminClusterService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable,
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
size={'small'}
bordered
dataSource={clusters}
size={'small'}
loading={showLoader}
columns={clusterColumns}
dataSource={filteredClusters}
pagination={defaultPagination}
onRowAdd={hasPermission('AdminCluster.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление куста')}
onRowEdit={hasPermission('AdminCluster.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование куста')}
onRowDelete={hasPermission('AdminCluster.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление куста')}
tableName={'admin_cluster_controller'}
/>
</LoaderPortal>
</>
)
})

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 {
EditableTable,
@ -8,7 +9,6 @@ import {
makeSelectColumn,
defaultPagination
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils'
@ -20,6 +20,11 @@ export const CompanyController = memo(() => {
const [columns, setColumns] = useState([])
const [companies, setCompanies] = useState([])
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 companies = await AdminCompanyService.getAll()
@ -53,7 +58,7 @@ export const CompanyController = memo(() => {
'Получение списка типов команд'
), [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminCompanyService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
@ -63,22 +68,30 @@ export const CompanyController = memo(() => {
`Не удалось обновить список компаний`,
'Получение списка компаний'
),
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полю Название)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
size={'small'}
bordered
size={'small'}
columns={columns}
dataSource={companies}
loading={showLoader}
dataSource={filteredCompanies}
pagination={defaultPagination}
onRowAdd={hasPermission('AdminCompany.edit') && makeActionHandler('insert', handlerProps, null, 'Добавлениее компаний')}
onRowEdit={hasPermission('AdminCompany.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование команий')}
onRowDelete={hasPermission('AdminCompany.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление компаний')}
tableName={'admin_company_controller'}
/>
</LoaderPortal>
</>
)
})

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 {
EditableTable,
@ -7,7 +8,6 @@ import {
makeStringSorter,
defaultPagination
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils'
@ -26,6 +26,11 @@ const columns = [
export const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([])
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(
async() => {
@ -39,27 +44,35 @@ export const CompanyTypeController = memo(() => {
useEffect(updateTable, [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminCompanyTypeService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable,
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полю Название)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
bordered
size={'small'}
columns={columns}
dataSource={companyTypes}
loading={showLoader}
pagination={defaultPagination}
dataSource={filteredCompanyTypes}
onRowAdd={hasPermission('AdminCompanyType.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление типа компаний')}
onRowEdit={hasPermission('AdminCompanyType.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование типа компаний')}
onRowDelete={hasPermission('AdminCompanyType.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление типа компаний')}
tableName={'admin_company_type_controller'}
/>
</LoaderPortal>
</>
)
})

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 { 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 { min1 } from '@utils/validationRules'
import { arrayOrDefault } from '@utils'
import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
@ -20,6 +20,14 @@ const depositColumns = [
export const DepositController = memo(() => {
const [deposits, setDeposits] = useState([])
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(
async() => {
@ -33,27 +41,35 @@ export const DepositController = memo(() => {
useEffect(updateTable, [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminDepositService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable,
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
size={'small'}
bordered
dataSource={deposits}
size={'small'}
loading={showLoader}
columns={depositColumns}
dataSource={filteredDeposits}
pagination={defaultPagination}
onRowAdd={hasPermission('AdminDeposit.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление месторождения')}
onRowEdit={hasPermission('AdminDeposit.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование месторождения')}
onRowDelete={hasPermission('AdminDeposit.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление месторождения')}
tableName={'admin_deposit_controller'}
/>
</LoaderPortal>
</>
)
})

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 {
EditableTable,
@ -6,7 +7,6 @@ import {
makeColumn,
makeStringSorter
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminPermissionService } from '@api'
import { arrayOrDefault } from '@utils'
@ -27,8 +27,15 @@ const columns = [
]
export const PermissionController = memo(() => {
const [showLoader, setShowLoader] = useState(false)
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(
async () => {
@ -42,27 +49,35 @@ export const PermissionController = memo(() => {
useEffect(() => updateTable(), [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminPermissionService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Описание)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
bordered
size={'small'}
columns={columns}
dataSource={permissions}
loading={showLoader}
dataSource={filteredPermissions}
pagination={{ showSizeChanger: true }}
onRowAdd={hasPermission('AdminPermission.edit') && makeActionHandler('insert', handlerProps, null, 'Добавление права')}
onRowEdit={hasPermission('AdminPermission.edit') && makeActionHandler('update', handlerProps, null, 'Редактирование права')}
onRowDelete={hasPermission('AdminPermission.delete') && makeActionHandler('delete', handlerProps, null, 'Удаление права')}
tableName={'admin_permission_controller'}
/>
</LoaderPortal>
</>
)
})

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

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 { memo, useEffect, useState } from 'react'
import { memo, useMemo } from 'react'
export const RoleTag = memo(({ roles, value, onChange }) => {
const [options, setOptions] = useState([])
useEffect(() => {
setOptions(roles.map((elm) => ({
const options = useMemo(() => roles.map((elm) => ({
key: Date.now(),
value: `${elm.caption}`,
label: elm.caption
})))
}, [roles])
})), [roles])
return (
<Select

View File

@ -1,16 +1,15 @@
import { Button, Input, Tag } from 'antd'
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 {
EditableTable,
makeColumn,
makeSelectColumn,
makeActionHandler,
makeStringSorter,
makeNumericSorter,
defaultPagination
defaultPagination,
makeTextColumn
} from '@components/Table'
import { RoleView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal'
@ -38,7 +37,7 @@ export const UserController = memo(() => {
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const filteredUsers = users.filter((user) => user && [
const filteredUsers = users.filter((user) => user && (!searchValue || [
user.login ?? '',
user.name ?? '',
user.surname ?? '',
@ -47,7 +46,7 @@ export const UserController = memo(() => {
user.phone ?? '',
user.position ?? '',
user.company?.caption ?? '',
].join(' ').toLowerCase().includes(searchValue))
].join(' ').toLowerCase().includes(searchValue.toLowerCase())))
setFilteredUsers(filteredUsers)
},
setIsSearching,
@ -106,8 +105,14 @@ export const UserController = memo(() => {
const filters = makeTextFilters(users, ['surname', 'name', 'patronymic', 'email'])
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([
makeColumn('Логин', 'login', {
makeTextColumn('Логин', 'login', null, null, null, {
editable: true,
formItemRules: [
{ required: true },
@ -121,59 +126,40 @@ export const UserController = memo(() => {
// })
// TODO: Для проверки уникальности логина необходимо исключить из выборки логин выбранного пользователя
],
sorter: makeStringSorter('login'),
}),
makeColumn('Фамилия', 'surname', {
makeTextColumn('Фамилия', 'surname', filters.surname, null, null, {
editable: true,
formItemRules: [{ required: true }, ...nameRules],
sorter: makeStringSorter('surname'),
filters: filters.surname,
filterSearch: true,
onFilter: makeTextOnFilter('surname'),
}),
makeColumn('Имя', 'name', {
makeTextColumn('Имя', 'name', filters.name, null, null, {
editable: true,
formItemRules: nameRules,
sorter: makeStringSorter('name'),
filters: filters.name,
filterSearch: true,
onFilter: makeTextOnFilter('name'),
}),
makeColumn('Отчество', 'patronymic', {
makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, {
editable: true,
formItemRules: nameRules,
sorter: makeStringSorter('patronymic'),
filters: filters.patronymic,
filterSearch: true,
onFilter: makeTextOnFilter('patronymic'),
}),
makeColumn('E-mail', 'email', {
makeTextColumn('E-mail', 'email', filters.email, null, null, {
editable: true,
formItemRules: [{ required: true }, ...emailRules],
sorter: makeStringSorter('email'),
filters: filters.email,
filterSearch: true,
onFilter: makeTextOnFilter('email'),
}),
makeColumn('Номер телефона', 'phone', {
makeTextColumn('Номер телефона', 'phone', null, null, null, {
editable: true,
formItemRules: phoneRules,
sorter: makeStringSorter('phone'),
}),
makeColumn('Должность', 'position', {
editable: true,
sorter: makeStringSorter('position'),
}),
makeColumn('Роли', 'roleNames', {
makeTextColumn('Должность', 'position', null, null, null, { editable: true }),
makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
editable: true,
input: <RoleTag roles={roles} />,
filters: roleFilters,
onFilter: makeArrayOnFilter('roleNames'),
render: (item) => item?.map((elm) => (
<Tag key={elm} color={'blue'}>
<RoleView role={roles.find((role) => role.caption === elm)} />
</Tag>
)) ?? '-'
}),
makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true,
@ -186,12 +172,12 @@ export const UserController = memo(() => {
'Получение списка компаний'
), [])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminUserService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable,
}
}), [updateTable])
const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject])
@ -200,7 +186,7 @@ export const UserController = memo(() => {
<LoaderPortal show={showLoader}>
<Input.Search
allowClear
placeholder={'Поиск пользователей'}
placeholder={'Введите текст для поиска (по всем полям за исключением ролей)...'}
onChange={onSearchTextChange}
style={{ marginBottom: '15px' }}
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 { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
import { RequestTrackerService } from '@api'
@ -12,14 +12,22 @@ const columns = [
makeColumn('Логин', 'login', { sorter: makeStringSorter('login') }),
makeColumn('IP', 'ip', { sorter: makeStringSorter('ip') }),
makeColumn('Дата посещения', 'lastDate', {
render: (date) => formatDate(date, false, 'DD MMM YYYY, HH:mm:ss'),
render: (date) => formatDate(date, false),
sorter: makeDateSorter('lastDate'),
}),
]
export const VisitLog = memo(() => {
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(
async () => {
@ -27,23 +35,30 @@ export const VisitLog = memo(() => {
logData.forEach((log) => log.key = `${log.login}${log.ip}`)
setLogData(logData)
},
setIsLoading,
setShowLoader,
`Не удалось загрузить список последних посещений пользователей`,
'Получение списка последних посещений'
), [])
return (
<LoaderPortal show={isLoading}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<Table
size={'small'}
bordered
size={'small'}
columns={columns}
dataSource={logData}
loading={showLoader}
dataSource={filteredLogData}
pagination={defaultPagination}
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 { memo, useEffect, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import {
AdminClusterService,
@ -19,16 +19,13 @@ import {
defaultPagination,
makeTimezoneColumn,
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views'
import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { coordsFixed } from '../DepositController'
import TelemetrySelect from './TelemetrySelect'
import '@styles/admin.css'
const wellTypes = [
{ value: 1, label: 'Наклонно-направленная' },
@ -44,8 +41,17 @@ export const WellController = memo(() => {
const [columns, setColumns] = useState([])
const [wells, setWells] = useState([])
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 () => {
const wells = await AdminWellService.getAll()
setWells(arrayOrDefault(wells))
@ -53,11 +59,11 @@ export const WellController = memo(() => {
setShowLoader,
`Не удалось загрузить список скважин`,
'Получение списка скважин'
)
), [])
const duplicateWell = (well) => {
const duplicateWell = useCallback((well) => {
// TODO: Метод дубликации скважины
}
}, [])
const addititonalButtons = memo((record, editingKey) => (
<Button
@ -112,22 +118,30 @@ export const WellController = memo(() => {
setShowLoader,
`Не удалось загрузить список кустов`,
'Получение списка кустов'
), [])
), [updateTable])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: AdminWellService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateTable
}
}), [updateTable])
return (
<LoaderPortal show={showLoader}>
<>
<Input.Search
style={{ margin: '15px 0' }}
placeholder={'Введите текст для поиска (по полям: Название, Долгота, Широта)...'}
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
loading={showLoader}
/>
<EditableTable
size={'small'}
bordered
size={'small'}
columns={columns}
dataSource={wells}
loading={showLoader}
dataSource={filteredWells}
pagination={defaultPagination}
onRowAdd={hasPermission('AdminWell.edit') && makeActionHandler('insert', handlerProps, recordParser, 'Добавление скважины')}
onRowEdit={hasPermission('AdminWell.edit') && makeActionHandler('update', handlerProps, recordParser, 'Редактирование скважины')}
@ -136,7 +150,7 @@ export const WellController = memo(() => {
buttonsWidth={95}
tableName={'admin_well_controller'}
/>
</LoaderPortal>
</>
)
})

View File

@ -1,5 +1,5 @@
import { Layout, Menu } from 'antd'
import { lazy, Suspense } from 'react'
import { lazy, memo, Suspense } from 'react'
import { Switch, useParams } from 'react-router-dom'
import { PrivateMenuItem, PrivateRoute, PrivateDefaultRoute } from '@components/Private'
@ -14,12 +14,12 @@ const WellController = lazy(() => import( './WellController'))
const RoleController = lazy(() => import( './RoleController'))
const CompanyTypeController = lazy(() => import('./CompanyTypeController'))
const PermissionController = lazy(() => import( './PermissionController'))
const TelemetryController = lazy(() => import( './TelemetryController'))
const TelemetrySection = lazy(() => import( './Telemetry'))
const VisitLog = lazy(() => import( './VisitLog'))
const rootPath = '/admin'
export const AdminPanel = () => {
export const AdminPanel = memo(() => {
const { tab } = useParams()
return (
@ -33,7 +33,7 @@ export const AdminPanel = () => {
<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={'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={'Журнал посещений'} />
</Menu>
@ -49,7 +49,7 @@ export const AdminPanel = () => {
<PrivateRoute path={`${rootPath}/company_type` } component={CompanyTypeController} />
<PrivateRoute path={`${rootPath}/role` } component={ RoleController} />
<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} />
<PrivateDefaultRoute urls={[
`${rootPath}/deposit`,
@ -69,6 +69,6 @@ export const AdminPanel = () => {
</Layout>
</Layout>
)
}
})
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 LoaderPortal from '@components/LoaderPortal'
import { WellSelector } from '@components/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { WellSelector } from '@components/selectors/WellSelector'
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils'
@ -18,10 +18,17 @@ const { Cell, Row } = Summary
const numericRender = makeNumericRender()
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, [
makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100),
makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100),
makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100, {
sorter: makeSectionSorter(key, 'depth'),
}),
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 = [
@ -90,21 +97,30 @@ export const Statistics = memo(({ idWell }) => {
async () => {
const types = await WellOperationService.getSectionTypes(idWell)
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([
...defaultColumns,
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`)),
...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`)),
])
setCmpColumns([
...defaultColumns,
...Object.entries(types).map(([id, name]) => makeSectionColumn(name, `section_${id}`, {
...filteredSections.map(([id, name]) => makeSectionColumn(name, `section_${id}`, {
speedRender: cmpSpeedRender(`section_${id}`)
}))
])
},
setIsPageLoading,
`Не удалось получить типы секции`,
`Получение списка возможных секций`,
), [idWell, cmpSpeedRender])
'Не удалось установить необходимые столбцы'
), [sectionTypes, avgData, cmpSpeedRender])
useEffect(() => invokeWebApiWrapperAsync(
async () => {

View File

@ -1,5 +1,5 @@
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 { 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
export const WellCompositeSections = memo(({ idWell, statsWells, selectedSections }) => {
const [rows, setRows] = useState([])
const [params, setParams] = useState([])
const [paramsColumns, setParamsColumns] = useState([])
const [selectedWells, setSelectedWells] = useState([])
const [wellOperations, setWellOperations] = useState([])
const [selectedWellsKeys, setSelectedWellsKeys] = useState([])
@ -44,21 +42,9 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
const location = useLocation()
useEffect(() => (async() => setParamsColumns(await getColumns(idWell)))(), [idWell])
const paramsColumns = useMemo(async() => await getColumns(idWell), [idWell])
useEffect(() => {
if (isOpsModalVisible || selectedWellId <= 0) return
invokeWebApiWrapperAsync(
async () => {
const { operations } = await getOperations(selectedWellId)
setWellOperations(operations)
},
setShowLoader,
`Не удалось загрузить операции по скважине "${selectedWellId}"`,
)
}, [selectedWellId, isOpsModalVisible])
useEffect(() => {
const rows = useMemo(() => {
const rows = []
statsWells?.forEach((well) => {
well.sections?.forEach((section) => {
@ -113,9 +99,21 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
'nonProductiveTimeFact',
])
setRows(rows)
return rows
}, [statsWells])
useEffect(() => {
if (isOpsModalVisible || selectedWellId <= 0) return
invokeWebApiWrapperAsync(
async () => {
const { operations } = await getOperations(selectedWellId)
setWellOperations(operations)
},
setShowLoader,
`Не удалось загрузить операции по скважине "${selectedWellId}"`,
)
}, [selectedWellId, isOpsModalVisible])
useEffect(() => {
const selected = rows.filter((row) => selectedSections.some(section => (
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))
}, [rows, selectedSections])
const columns = [
const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null,
(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>
)) ?? '-',
},
]
], [location.pathname])
const rowSelection = hasPermission('WellOperation.edit') && {
const rowSelection = useMemo(() => hasPermission('WellOperation.edit') && {
selectedRowKeys: selectedWellsKeys,
onChange: (keys, items) => invokeWebApiWrapperAsync(
async () => {
@ -187,7 +185,7 @@ export const WellCompositeSections = memo(({ idWell, statsWells, selectedSection
`Не удалось сохранить изменения выбранных секций для композитной скважины "${idWell}"`,
'Изменение выбранных секций скважины'
)
}
}, [idWell, selectedWellsKeys])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => {

View File

@ -8,7 +8,7 @@ import {
} from '@api'
import { arrayOrDefault } from '@utils'
import LoaderPortal from '@components/LoaderPortal'
import WellSelector from '@components/WellSelector'
import WellSelector from '@components/selectors/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory'
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 { Switch, useParams } from 'react-router-dom'
@ -9,7 +9,7 @@ import Statistics from './Statistics'
export const Analytics = memo(({ idWell }) => {
const { tab } = useParams()
const rootPath = `/well/${idWell}/analytics`
const rootPath = useMemo(() => `/well/${idWell}/analytics`, [idWell])
return (
<Layout>

View File

@ -1,21 +1,14 @@
import { useEffect, useState } from 'react'
import { memo, useMemo } from 'react'
import { Grid, GridItem } from '@components/Grid'
import { Column } from '@components/charts/Column'
export const ArchiveColumn = ({ lineGroup, data, interval, style, headerHeight, yStart }) => {
const [lineGroupWithoutShapes, setLineGroupWithoutShapes] = useState([])
const [pv, setPV] = useState([])
useEffect(() => {
const lgws = lineGroup.filter(cfg => !cfg.isShape)
setLineGroupWithoutShapes(lgws)
setPV(lgws.filter(line => line.showLabels).map(line => ({
export const ArchiveColumn = memo(({ lineGroup, data, interval, style, headerHeight, yStart }) => {
const lgws = useMemo(() => lineGroup.filter(cfg => !cfg.isShape), [lineGroup])
const pv = useMemo(() => lgws.filter(line => line.showLabels).map(line => ({
color: line.color,
label: line.label
})))
}, [lineGroup])
})), [lgws])
return (
<div style={style}>
@ -26,13 +19,13 @@ export const ArchiveColumn = ({ lineGroup, data, interval, style, headerHeight,
</Grid>
<Column
data={data}
lineGroup={lineGroupWithoutShapes}
lineGroup={lgws}
interval={interval}
yDisplay={false}
yStart={yStart}
/>
</div>
)
}
})
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'
@ -41,13 +41,9 @@ export const cutData = (data, beginDate, endDate) => {
return data
}
export const ArchiveDisplay = ({data, startDate, interval, onWheel}) => {
const [chartData, setChartData] = useState([])
useEffect(() => {
const endDate = new Date(+startDate + interval)
setChartData(cutData(data, startDate, endDate))
}, [data, startDate, interval])
export const ArchiveDisplay = memo(({data, startDate, interval, onWheel}) => {
const endDate = useMemo(() => new Date(+startDate + interval), [startDate, interval])
const chartData = useMemo(() => cutData(data, startDate, endDate), [data, startDate, endDate])
return (
<Grid onWheel={onWheel}>
@ -65,6 +61,6 @@ export const ArchiveDisplay = ({data, startDate, interval, onWheel}) => {
))}
</Grid>
)
}
})
export default ArchiveDisplay

View File

@ -1,11 +1,11 @@
/* 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 LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { TelemetryDataSaubService } from '@api'
import { normalizeData } from '@pages/TelemetryView'
@ -69,7 +69,7 @@ export const Archive = memo(({ idWell }) => {
const [showLoader, setShowLoader] = useState(false)
const [loaded, setLoaded] = useState(null)
const onGraphWheel = (e) => {
const onGraphWheel = useCallback((e) => {
if (loaded && dateLimit.from && dateLimit.to) {
setStartDate((prevStartDate) => {
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)))
})
}
}
}, [loaded, dateLimit, chartInterval])
const isDateDisabled = (date) => {
const isDateDisabled = useCallback((date) => {
if (!date) return false
const dt = new Date(date).setHours(0, 0, 0, 0)
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
}
const isDateTimeDisabled = (date) => ({
}, [dateLimit])
const isDateTimeDisabled = useCallback((date) => ({
disabledHours: () => range(0, 24).filter(h => {
if (!date) return false
const dt = +new Date(date).setHours(h)
@ -102,7 +103,7 @@ export const Archive = memo(({ idWell }) => {
const dt = +new Date(date).setSeconds(s)
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
})
})
}), [dateLimit])
useEffect(() => invokeWebApiWrapperAsync(
async () => {

View File

@ -1,5 +1,5 @@
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 { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
@ -108,7 +108,7 @@ export const ClusterWells = memo(({ statsWells }) => {
setTableData(data)
}, [statsWells])
const columns = [
const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null,
(_, item) => (
<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>
)) ?? '-',
},
]
], [location.pathname])
return (
<>

View File

@ -1,9 +1,9 @@
import { memo, useMemo } from 'react'
import { Table } from 'antd'
import { makeTextColumn, makeNumericColumnPlanFact } from '@components/Table'
import { getPrecision } from '@utils/functions'
export const WellOperationsTable = ({ wellOperations }) => {
const columns = [
makeTextColumn('Конструкция секции', 'sectionType'),
makeTextColumn('Операция', 'operationName'),
@ -12,7 +12,8 @@ export const WellOperationsTable = ({ wellOperations }) => {
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,
sectionType: el.plan?.wellSectionTypeName ?? el.fact?.wellSectionTypeName,
operationName: `${el.plan?.categoryName ?? el.fact?.categoryName ?? ''} ${' '}
@ -23,7 +24,7 @@ export const WellOperationsTable = ({ wellOperations }) => {
durationHoursFact: el.fact?.durationHours,
commentPlan: el.plan?.comment ?? '-',
commentFact: el.fact?.comment ?? '-'
}))
})), [wellOperations])
return (
<Table
@ -36,6 +37,6 @@ export const WellOperationsTable = ({ wellOperations }) => {
tableName={'well_operations'}
/>
)
}
})
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 { arrayOrDefault } from '@utils'
@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import ClusterWells from './ClusterWells'
export const Cluster = () => {
export const Cluster = memo(() => {
const { idCluster } = useParams()
const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false)
@ -28,6 +28,6 @@ export const Cluster = () => {
<ClusterWells statsWells={data} />
</LoaderPortal>
)
}
})
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 { FileService } from '@api'
import { hasPermission } from '@utils/permissions'
import LoaderPortal from '@components/LoaderPortal'
import { UploadForm } from '@components/UploadForm'
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 { formatDate } from '@utils'
import { hasPermission } from '@utils/permissions'
const pageSize = 12
const { RangePicker } = DatePicker
@ -24,26 +23,11 @@ const columns = [
{name}
</Button>
),
}, {
title: 'Дата загрузки',
key: 'uploadDate',
dataIndex: 'uploadDate',
render: item => formatDate(item, false, 'DD MMM YYYY, HH:mm:ss'),
}, {
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}/>
}
},
makeDateColumn('Дата загрузки', 'uploadDate'),
makeNumericColumn('Размер', 'size', null, null, formatBytes),
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> })
]
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 [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) => {
await FileService.delete(idWell, file.id)
update()
}
const hanleCompanySearch = (value, _) => setFilterCompanyName(value)
const hanleFileNameSearch = (value, _) => setFilterFileName(value)
const mergedColumns = [...columns, ...(customColumns ?? [])]
const update = () => {
const update = useCallback(() => {
let begin = null
let end = null
if (filterDataRange?.length > 1) {
@ -101,13 +77,15 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
`Не удалось загрузить файлы по скважине "${idWell}"`,
'Загрузка файла по скважине'
)
}
}, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page])
useEffect(update, [idWell, idCategory, page, filterDataRange, filterCompanyName, filterFileName])
useEffect(update, [update])
useEffect(() => onChange?.(files), [files, onChange])
const companies = [...new Set(files.map(file => file.company))].filter(company => company)
const filenames = [...new Set(files.map(file => file.name))].filter(name => name)
const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => {
await FileService.delete(idWell, file.id)
update()
}), [idWell, idCategory, update])
return (
<LoaderPortal show={showLoader}>
@ -124,7 +102,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
<Search
list={'listCompanies'}
placeholder={'Фильтр по компании'}
onSearch={hanleCompanySearch}
onSearch={setFilterCompanyName}
/>
<datalist id={'listCompanies'}>
{companies.map((company) => (
@ -138,7 +116,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
<Search
list={'listFileNames'}
placeholder={'Фильтр по имени файла'}
onSearch={hanleFileNameSearch}
onSearch={setFilterFileName}
/>
<datalist id={'listFileNames'}>
{filenames.map((name) => (
@ -154,7 +132,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
url={uploadUrl}
accept={accept}
onUploadStart={() => setShowLoader(true)}
onUploadComplete={handleUploadComplete}
onUploadComplete={update}
/>
</div>
)}
@ -169,9 +147,9 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus
pagination={{
...pagination,
showSizeChanger: false,
onChange: (page) => setPage(page),
onChange: setPage,
}}
onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete}
onRowDelete={handleFileDelete}
rowKey={(record) => record.id}
tableName={tableName ?? `file_${idCategory}`}
/>

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { UserView } from '@components/views'
@ -113,7 +113,7 @@ export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) =>
`Изменение статуса пользователя`
), [users, idWell, category.idFileCategory])
const userColumns = [
const userColumns = useMemo(() => [
makeColumn('Пользователь', 'user', {
sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0,
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])

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 {
DeleteOutlined,
@ -6,11 +6,11 @@ import {
TableOutlined,
} from '@ant-design/icons'
import Poprompt from '@components/Poprompt'
import { UserView } from '@components/views'
import UploadForm from '@components/UploadForm'
import DownloadLink from '@components/DownloadLink'
import LoaderPortal from '@components/LoaderPortal'
import Poprompt from '@components/selectors/Poprompt'
import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory'
import { DrillingProgramService } from '@api'
import { formatDate } from '@utils'
@ -44,7 +44,7 @@ export const CategoryRender = memo(({ idWell, partData, onUpdate, onEdit, onHist
file // Информация о файле
} = 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 [isDeleting, setIsDeleting] = useState(false)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import 'moment/locale/ru'
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 { ReportService } from '@api'
@ -43,7 +43,7 @@ export const Report = memo(({ idWell }) => {
const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false)
const handleReportCreation = async () => await invokeWebApiWrapperAsync(
const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync(
async () => {
const taskId = await ReportService.createReport(
idWell,
@ -81,9 +81,11 @@ export const Report = memo(({ idWell }) => {
${filterDateRange[0].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(
async () => {

View File

@ -6,7 +6,7 @@ import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
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 }) => {
const [expirePeriod, setExpirePeriod] = useState(defaultPeriod)

View File

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

View File

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

View File

@ -12,8 +12,8 @@ const style = { margin: 4 }
export const ImportExportBar = memo(({ idWell, onImported, disabled }) => {
const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const downloadTemplate = async () => download(`/api/well/${idWell}/wellOperations/template`)
const downloadExport = async () => download(`/api/well/${idWell}/wellOperations/export`)
const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`)
const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`)
const onDone = () => {
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 {
EditableTable,
@ -54,15 +54,15 @@ export const WellDrillParams = memo(({ idWell }) => {
await updateParams()
})(), [idWell, updateParams])
const handlerProps = {
const handlerProps = useMemo(() => ({
service: DrillParamsService,
setLoader: setShowLoader,
errorMsg: `Не удалось выполнить операцию`,
onComplete: updateParams,
idWell
}
}), [idWell, updateParams])
const recordParser = (record) => ({ ...record, idWell })
const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
return (
<LoaderPortal show={showLoader}>

View File

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

View File

@ -59,7 +59,7 @@ export const WellOperations = memo(({ idWell }) => {
<WellOperationsEditor idWell={idWell} idType={0} tableName={'well_operations_plan'}/>
</PrivateRoute>
<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 path={`${rootPath}/drillProcessFlow`}>
<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 const defaultFormat: string = 'YYYY.MM.DD HH:mm'
export const defaultFormat: string = 'DD.MM.YYYY HH:mm'
export enum timeInS {
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
}
export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate => {
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return NaN
export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate | null => {
if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return null
const d = new Date(date)
d.setMilliseconds(d.getMilliseconds() + value * timeInS[type] * 1000)
return d

View File

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

View File

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