Merge branch 'dev' into feature/CF2-72-AdminPanel

This commit is contained in:
Александр Сироткин 2021-12-17 12:54:00 +05:00
commit df0fe53049
10 changed files with 302 additions and 112 deletions

View File

@ -16,10 +16,12 @@ const timePeriodCollection = [
type PeriodPickerProps = {
defaultValue?: number
onChange?: (value: number) => void
[other: string]: any
}
export const PeriodPicker = ({ defaultValue = defaultPeriod, onChange }: PeriodPickerProps) => (
export const PeriodPicker = ({ defaultValue = defaultPeriod, onChange, ...other }: PeriodPickerProps) => (
<Select
{...other}
defaultValue={defaultValue}
onChange={(value) => onChange?.(Number(value))}
options={timePeriodCollection}

View File

@ -32,7 +32,7 @@ export const EditableCell = ({
className={formItemClass}
rules={formItemRules ?? [{
required: isRequired,
message: `Please Input ${title}!`,
message: `Пожалуйста, введите ${title}!`,
}]}
initialValue={initialValue}
>

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react'
import { Select, Table as RawTable } from 'antd'
import { InputNumber, Select, Table as RawTable } from 'antd'
import { OptionsType } from 'rc-select/lib/interface'
import { tryAddKeys } from './EditableTable'
import { makeNumericSorter, makeStringSorter} from './sorters'
@ -143,6 +143,7 @@ export const makeNumericColumn = (
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
sorter: makeNumericSorter(dataIndex),
width: width,
input: <InputNumber style={{ width: '100%' }}/>,
render: renderDelegate ?? makeNumericRender(),
align: 'right',
...other

View File

@ -9,7 +9,7 @@ interface UserViewProps {
export const UserView = ({ user } : UserViewProps) => (user ? (
<Tooltip title={(
<Grid columnGap='8px'>
<Grid style={{ columnGap: '8px' }}>
<GridItem row={1} col={1}>Фамилия:</GridItem>
<GridItem row={1} col={2}>{user?.surname}</GridItem>
<GridItem row={2} col={1}>Имя:</GridItem>

View File

@ -0,0 +1,113 @@
import { useState } from 'react'
import { Select, Modal, Input, InputNumber } from 'antd'
import { SetpointsService } from '../../services/api'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { makeNumericRender, EditableTable, makeNumericInput } from '../../components/Table'
import { PeriodPicker, defaultPeriod } from '../../components/PeriodPicker'
import { Grid, GridItem } from '../../components/Grid'
export const SetpointSender = ({ idWell, onClose, visible, setpointNames }) => {
const [expirePeriod, setExpirePeriod] = useState(defaultPeriod)
const [comment, setComment] = useState('')
const [setpoints, setSetpoints] = useState([])
const [isLoading, setIsLoading] = useState(false)
const addingColumns = [
{
title: 'Наименование уставки',
dataIndex: 'name',
editable: true,
isRequired: true,
width: 200,
input: <Select options={setpointNames} />,
render: (val) => setpointNames.find((name) => name.value === val)?.label
}, {
title: 'Значение',
dataIndex: 'value',
editable: true,
isRequired: true,
width: 125,
input: <InputNumber style={{ width: '100%' }} formatter={value => `${value}`.replace(',', '.')}/>,
render: makeNumericRender(1),
align: 'right'
}
]
const onAdd = async (sp) => setSetpoints((prevSp) => {
sp.key = Date.now()
prevSp.push(sp)
return prevSp
})
const onEdit = async (sp) => setSetpoints((prevSp) => {
const idx = prevSp.findIndex((val) => val.key === sp.key)
prevSp[idx] = sp
return prevSp
})
const onDelete = async (sp) => setSetpoints((prevSp) => {
const idx = prevSp.findIndex((val) => val.key === sp.key)
prevSp.splice(idx, 1)
return prevSp
})
const onModalOk = () => invokeWebApiWrapperAsync(
async () => {
const setpointsObject = setpoints.reduce((obj, sp) => (obj[sp.name] = sp.value, obj), {})
const request = {
uploadDate: new Date(),
obsolescenceSec: expirePeriod,
setpoints: setpointsObject,
comment: comment
}
await SetpointsService.insert(idWell, request)
onClose(true)
},
setIsLoading,
`Не удалось отправить уставки по скважине "${idWell}"`
)
return (
<Modal
width={800}
title={'Рекомендовать уставки'}
visible={visible}
onCancel={onClose}
onOk={onModalOk}
okText={'Отправить'}
>
<LoaderPortal show={isLoading}>
<Grid>
<GridItem row={1} col={1}>Период актуальности рекомендаций:</GridItem>
<GridItem row={1} col={2}>
<PeriodPicker style={{ marginLeft: 'auto' }} onChange={setExpirePeriod} width={'100%'}/>
</GridItem>
<GridItem row={2} col={1}>Комментарий:</GridItem>
<GridItem row={3} col={1} colSpan={3}>
<Input.TextArea
rows={4}
onChange={(e) => setComment(e.target.value)}
value={comment}
required
/>
</GridItem>
<GridItem row={4} col={1} colSpan={3}>
<EditableTable
bordered
columns={addingColumns}
dataSource={setpoints}
onRowAdd={onAdd}
onRowEdit={onEdit}
onRowDelete={onDelete}
pagination={false}
style={{ margin: '10px 0' }}
/>
</GridItem>
</Grid>
</LoaderPortal>
</Modal>
)
}

View File

@ -0,0 +1,77 @@
import moment from 'moment'
import { memo } from 'react'
import { Modal, Input } from 'antd'
import { Table } from '../../components/Table'
import { UserView } from '../../components/UserView'
import { Grid, GridItem } from '../../components/Grid'
import { periodToString } from '../../utils/datetime'
export const setpointStatus = {
0: 'Неизвестно',
1: 'Ожидает отправки',
2: 'Отправлено',
3: 'Принято оператором',
4: 'Отклонено оператором',
5: 'Устарело',
}
export const getSetpointStatus = (id) => {
if (!id || Object.keys(setpointStatus).every((idx) => Number(idx) !== id))
return setpointStatus[0]
return setpointStatus[id]
}
const columns = [
{ title: 'Название', dataIndex: 'name' },
{ title: 'Значение', dataIndex: 'value' },
]
export const SetpointViewer = memo(({ setpoint, visible, onClose, setpointNames }) => {
let date = moment(setpoint?.uploadDate).format('DD MMM YYYY, HH:mm:ss')
let setpoints = []
if (setpoint) {
setpoints = Object.keys(setpoint?.setpoints).map((name) => ({
name: setpointNames.find(spName => spName.value === name)?.label,
value: setpoint.setpoints[name]
}))
}
return (
<Modal
width={800}
title={`Уставка от ${date}`}
onCancel={onClose}
visible={visible}
footer={null}
>
<Grid>
<GridItem row={1} col={1}>Дата рекомендаций:</GridItem>
<GridItem row={1} col={2}>{date}</GridItem>
<GridItem row={2} col={1}>Автор:</GridItem>
<GridItem row={2} col={2}><UserView user={setpoint?.author}/></GridItem>
<GridItem row={3} col={1}>Статус:</GridItem>
<GridItem row={3} col={2}>{getSetpointStatus(setpoint?.idState)}</GridItem>
<GridItem row={4} col={1}>Период актуальности рекомендаций:</GridItem>
<GridItem row={4} col={2}>{periodToString(setpoint?.obsolescenceSec)}</GridItem>
<GridItem row={5} col={1}>Комментарий:</GridItem>
<GridItem row={6} col={1} colSpan={3}>
<Input.TextArea rows={4} value={setpoint?.comment} readOnly />
</GridItem>
<GridItem row={7} col={1} colSpan={3}>
<Table
bordered
size={'small'}
dataSource={setpoints}
columns={columns}
pagination={false}
/>
</GridItem>
</Grid>
</Modal>
)
})

View File

@ -1,41 +1,25 @@
import { Button, Input, Modal, Select } from 'antd'
import { useState } from 'react'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import moment from 'moment'
import { Button, Modal } from 'antd'
import { useState, useEffect } from 'react'
import { Table } from '../../components/Table'
import { UserView } from '../../components/UserView'
import LoaderPortal from '../../components/LoaderPortal'
import PeriodPicker, { defaultPeriod } from '../../components/PeriodPicker'
import { EditableTable, makeNumericRender } from '../../components/Table'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { makeStringCutter } from '../../utils/string'
import { SetpointsService } from '../../services/api'
import { SetpointSender } from './SetpointSender'
import { SetpointViewer, getSetpointStatus } from './SetpointViewer'
export const Setpoints = ({ idWell, ...other }) => {
const [isModalShown, setIsModalShown] = useState(false)
const [isModalVisible, setIsModalVisible] = useState(false)
const [isSenderVisible, setIsSenderVisible] = useState(false)
const [isViewerVisible, setIsViewerVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [setpointNames, setSetpointNames] = useState([])
const [setpoints, setSetpoints] = useState([])
const [comment, setComment] = useState('')
const [expirePeriod, setExpirePeriod] = useState(defaultPeriod)
const [selected, setSelected] = useState(null)
const [setpointNames, setSetpointNames] = useState([])
const columns = [
{
title: 'Наименование установки',
dataIndex: 'name',
editable: true,
isRequired: true,
width: 200,
input: <Select options={setpointNames} />,
render: (val) => setpointNames.find((name) => name.value === val)?.label
}, {
title: 'Значение',
dataIndex: 'value',
editable: true,
isRequired: true,
width: 125,
render: makeNumericRender(7),
align: 'right'
}
]
const onOpenClick = () => invokeWebApiWrapperAsync(
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const names = await SetpointsService.getSetpointsNamesByIdWell(idWell)
if (!names) throw Error('Setpoints not found')
@ -44,84 +28,79 @@ export const Setpoints = ({ idWell, ...other }) => {
value: spn.name,
tooltip: spn.comment
})))
setIsModalShown(true)
},
setIsLoading,
`Не удалось загрузить список имёт уставок по скважине "${idWell}"`
), [idWell])
const showMore = (id) => {
const selected = setpoints.find((sp) => sp.id === id)
setSelected(selected ?? {})
setIsViewerVisible(true)
}
const historyColumns = [
{ title: 'Дата', dataIndex: 'uploadDate', render: item => moment(item).format('DD MMM YYYY, HH:mm:ss') },
{ title: 'Автор', dataIndex: 'author', render: (user) => <UserView user={user} /> },
{ title: 'Комментарий', dataIndex: 'comment', render: makeStringCutter() },
{ title: 'Статус', dataIndex: 'idState', render: (id) => getSetpointStatus(parseInt(id)) },
{ dataIndex: 'id', render: (id) => <Button onClick={() => showMore(id)}>Подробнее</Button> },
]
const updateTable = () => invokeWebApiWrapperAsync(
async () => {
const setpoints = await SetpointsService.getByIdWell(idWell)
setSetpoints(setpoints)
},
setIsLoading,
`Не удалось загрузить список для скважины "${idWell}"`
)
const onModalOk = () => invokeWebApiWrapperAsync(
async () => {
// eslint-disable-next-line no-sequences
const setpointsObject = setpoints.reduce((obj, sp) => (obj[sp.name] = sp.value, obj), {})
const request = {
uploadDate: new Date(),
obsolescenceSec: expirePeriod,
setpoints: setpointsObject,
comment: comment
useEffect(updateTable, [idWell])
const onSenderClose = (pushed) => {
if (pushed) updateTable()
setIsSenderVisible(false)
}
await SetpointsService.insert(idWell, request)
setIsModalShown(false)
},
setIsUploading,
`Не удалось отправить рекомендации по скважине "${idWell}"`
)
const onAdd = async (setpoint) => setSetpoints((prevSetpoints) => {
setpoint.key = Date.now()
prevSetpoints.push(setpoint)
return prevSetpoints
})
const onEdit = async (setpoint) => setSetpoints((prevSetpoints) => {
const idx = prevSetpoints.findIndex((val) => val.key === setpoint.key)
prevSetpoints[idx] = setpoint
return prevSetpoints
})
const onDelete = async (setpoint) => setSetpoints((prevSetpoints) => {
const idx = prevSetpoints.findIndex((val) => val.key === setpoint.key)
prevSetpoints.splice(idx, 1)
return prevSetpoints
})
return (
<div {...other}>
<Button onClick={onOpenClick} loading={isLoading}>
Рекомендовать установки
<Button onClick={() => setIsModalVisible(true)} loading={isLoading}>
Рекомендованные уставки
</Button>
<Modal
width={800}
title={'Рекомендация установок'}
visible={isModalShown}
onCancel={() => setIsModalShown(false)}
onOk={onModalOk}
okText={'Отправить'}
width={1200}
title={'Рекомендованные уставки'}
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={
<Button onClick={() => setIsSenderVisible(true)}>
Рекомендовать
</Button>
}
>
<LoaderPortal show={isUploading}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
Период актуальности рекомендаций:
<PeriodPicker onChange={setExpirePeriod} />
</div>
<EditableTable
<LoaderPortal show={isLoading}>
<Table
size={'small'}
bordered
columns={columns}
columns={historyColumns}
dataSource={setpoints}
onRowAdd={onAdd}
onRowEdit={onEdit}
onRowDelete={onDelete}
pagination={false}
style={{ margin: '10px 0' }}
/>
Комментарий:
<Input.TextArea
rows={4}
onChange={(e) => setComment(e.value)}
value={comment}
required
/>
</LoaderPortal>
</Modal>
<SetpointSender
idWell={idWell}
setpointNames={setpointNames}
visible={isSenderVisible}
onClose={onSenderClose}
/>
<SetpointViewer
setpointNames={setpointNames}
setpoint={selected}
visible={isViewerVisible}
onClose={() => setIsViewerVisible(false)}
/>
</div>
)
}

View File

@ -42,7 +42,7 @@ const blockHeightGroup = [
color: '#333',
showLabels: true
}, {
label: 'wellDepth',
label: 'Глубина скважины',
units: 'м',
xAccessorName: 'wellDepth',
yAccessorName: 'date',
@ -59,7 +59,7 @@ const blockHeightGroup = [
showLabels: true,
showLine: true
}, {
label: 'flowLimits',
label: 'Предел расхода',
units: 'л/с',
xAccessorName: 'flow',
yAccessorName: 'date',
@ -77,7 +77,7 @@ const blockSpeedGroup = [
color: '#0a0',
showLabels: true,
}, {
label: 'blockSpeedSp',
label: 'Скорость заданная',
units: 'м/ч',
xAccessorName: 'blockSpeedSp',
yAccessorName: 'date',
@ -96,7 +96,7 @@ const pressureGroup = [
color: '#c00',
showLabels: true
}, {
label: 'pressureSp',
label: 'Давление заданное',
units: 'атм',
xAccessorName: 'pressureSp',
yAccessorName: 'date',
@ -104,7 +104,7 @@ const pressureGroup = [
footer: 'SP',
dash
}, {
label: 'pressureIdle',
label: 'Давление ХХ',
units: 'атм',
xAccessorName: 'pressureIdle',
yAccessorName: 'date',
@ -112,7 +112,7 @@ const pressureGroup = [
footer: 'IDLE',
dash
}, {
label: 'pressureDeltaLimitMax',
label: 'Перепад давления максимальный',
units: 'атм',
xAccessorName: 'pressureDeltaLimitMax',
yAccessorName: 'date',
@ -120,7 +120,7 @@ const pressureGroup = [
footer: true,
dash
}, {
label: 'pressureLimits',
label: 'Предел давления заданный',
units: 'атм',
xAccessorName: 'pressure',
yAccessorName: 'date',
@ -138,7 +138,7 @@ const axialLoadGroup = [
color: '#00a',
showLabels: true
}, {
label: 'axialLoadSp',
label: 'Осевая нагрузка заданная',
units: 'т',
xAccessorName: 'axialLoadSp',
yAccessorName: 'date',
@ -146,7 +146,7 @@ const axialLoadGroup = [
footer: 'SP',
dash
}, {
label: 'axialLoadLimitMax',
label: 'Осевая нагрузка максимальная',
units: 'т',
xAccessorName: 'axialLoadLimitMax',
yAccessorName: 'date',
@ -154,7 +154,7 @@ const axialLoadGroup = [
footer: true,
dash
}, {
label: 'axialLoadLimits',
label: 'Пределы осевой нагрузки',
units: 'т',
xAccessorName: 'axialLoad',
yAccessorName: 'date',
@ -172,7 +172,7 @@ const hookWeightGroup = [
color: '#0aa',
showLabels: true
}, {
label: 'hookWeightIdle',
label: 'Вес инструмента ХХ',
units: 'т',
xAccessorName: 'hookWeightIdle',
yAccessorName: 'date',
@ -180,7 +180,7 @@ const hookWeightGroup = [
footer: 'IDLE',
dash
}, {
label: 'hookWeightLimitMin',
label: 'Вес инструмента минимальный',
units: 'т',
xAccessorName: 'hookWeightLimitMin',
yAccessorName: 'date',
@ -188,7 +188,7 @@ const hookWeightGroup = [
footer: true,
dash
}, {
label: 'hookWeightLimitMax',
label: 'Вес инструмента максимальный',
units: 'т',
xAccessorName: 'hookWeightLimitMax',
yAccessorName: 'date',
@ -203,7 +203,7 @@ const hookWeightGroup = [
color: '#aa0',
showLabels: true
}, {
label: 'rotorSpeedLimits',
label: 'Скорость вращения ВСП максимальная',
units: 'об/мин',
xAccessorName: 'rotorSpeed',
yAccessorName: 'date',
@ -237,7 +237,7 @@ const rotorTorqueGroup = [
footer: 'IDLE',
dash
}, {
label: 'rotorTorqueLimitMax',
label: 'Момент максимальный',
units: 'кН·м',
xAccessorName: 'rotorTorqueLimitMax',
yAccessorName: 'date',
@ -245,7 +245,7 @@ const rotorTorqueGroup = [
footer: true,
dash
}, {
label: 'rotorTorqueLimits',
label: 'Ограничения момента',
units: 'кН·м',
xAccessorName: 'rotorTorque',
yAccessorName: 'date',

10
src/utils/datetime.ts Normal file
View File

@ -0,0 +1,10 @@
export const periodToString = (time?: number) => {
if (!time) return '00:00:00'
const hours = Math.floor(time / 3600)
const minutes = Math.floor(time / 60 - hours * 60)
const seconds = Math.floor(time - hours * 3600 - minutes * 60)
const toFixed = (num: number) => `${num}`.padStart(2, '0')
return `${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
}

8
src/utils/string.ts Normal file
View File

@ -0,0 +1,8 @@
export const makeStringCutter = (maxLength = 100, separator = ' ', suffix = '...') => (comment?: string) => {
if (!comment || comment.length < maxLength)
return comment
let lastSep = comment.lastIndexOf(separator, maxLength)
if (lastSep < 0)
return comment.substring(0, maxLength - suffix.length) + suffix
return comment.substring(0, lastSep)
}