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

This commit is contained in:
Александр Сироткин 2021-12-27 15:31:37 +05:00
commit 8552a40506
25 changed files with 670 additions and 703 deletions

View File

@ -1,6 +1,6 @@
import { DatePicker } from 'antd' import { DatePicker } from 'antd'
import moment from 'moment' import moment from 'moment'
import { formatDate } from './index' import { defaultFormat } from '../../utils'
export type DatePickerWrapperProps = { export type DatePickerWrapperProps = {
value: moment.Moment, value: moment.Moment,
@ -13,7 +13,7 @@ export const DatePickerWrapper: React.FC<DatePickerWrapperProps> = ({value, onCh
allowClear={false} allowClear={false}
defaultValue={moment()} defaultValue={moment()}
value={moment.utc(value).local()} value={moment.utc(value).local()}
format={formatDate} format={defaultFormat}
showTime showTime
onChange={(date) => onChange(date)} onChange={(date) => onChange(date)}
{...other} {...other}

View File

@ -6,17 +6,25 @@ import { invokeWebApiWrapperAsync } from '../factory'
const newRowKeyValue = 'newRow' const newRowKeyValue = 'newRow'
export const makeActionHandler = (action, { service, setLoader, errorMsg, onComplete }) => service && action && ( export const makeActionHandler = (action, { idWell, service, setLoader, errorMsg, onComplete }, recordParser) => service && action && (
(record) => invokeWebApiWrapperAsync( (record) => invokeWebApiWrapperAsync(
async () => { async () => {
const addIdWell = (...params) => idWell ? [idWell, ...params] : params
if (typeof recordParser === 'function')
record = recordParser(record)
if (action === 'insert') { if (action === 'insert') {
record.key = Date.now() record.key = Date.now()
await service.insert(record) await service.insert(...addIdWell(record))
} else if (action === 'insertRange') {
if (!Array.isArray(record))
record = [record]
await service.insertRange(...addIdWell(record))
} else if (record.id) { } else if (record.id) {
if (action === 'update') if (action === 'update')
await service.put(record.id, record) await service.put(...addIdWell(record.id, record))
else if (action === 'delete') else if (action === 'delete')
await service.delete(record.id) await service.delete(...addIdWell(record.id))
} }
if (onComplete) if (onComplete)
await onComplete() await onComplete()

View File

@ -10,7 +10,6 @@ export { DatePickerWrapper } from './DatePickerWrapper'
export { SelectFromDictionary } from './SelectFromDictionary' export { SelectFromDictionary } from './SelectFromDictionary'
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/ export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
export const formatDate = 'YYYY.MM.DD HH:mm'
export const makeNumericRender = (fixed?: number) => (value: any, row: object): ReactNode => { export const makeNumericRender = (fixed?: number) => (value: any, row: object): ReactNode => {
let val = '-' let val = '-'
@ -209,7 +208,7 @@ export const makeSelectColumn = <T extends unknown = string>(
other?: columnPropsOther other?: columnPropsOther
) => makeColumn(title, dataIndex, { ) => makeColumn(title, dataIndex, {
input: <Select options={options}/>, input: <Select options={options}/>,
render: (value) => options.find(option => option?.value === value)?.label ?? defaultValue ?? '--', render: (value) => options?.find(option => option?.value === value)?.label ?? defaultValue ?? value ?? '--',
...other ...other
}) })

View File

@ -2,14 +2,17 @@ import { TreeSelect } from 'antd'
import { DefaultValueType } from 'rc-tree-select/lib/interface' import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode } from 'react' import { useState, useEffect, ReactNode } from 'react'
import { useHistory, useRouteMatch } from 'react-router-dom' import { useHistory, useRouteMatch } from 'react-router-dom'
import LoaderPortal from './LoaderPortal'
import { DepositService } from '../services/api' import { DepositService } from '../services/api'
import { invokeWebApiWrapperAsync } from './factory'
import { WellIcon, WellIconState } from './icons'
import { ReactComponent as DepositIcon } from '../images/DepositIcon.svg' import { ReactComponent as DepositIcon } from '../images/DepositIcon.svg'
import { ReactComponent as ClusterIcon } from '../images/ClusterIcon.svg' import { ReactComponent as ClusterIcon } from '../images/ClusterIcon.svg'
import { DepositDto } from '../services/api' import { DepositDto } from '../services/api'
import { RawDate } from '../utils/DateTimeUtils' import { RawDate } from '../utils'
import LoaderPortal from './LoaderPortal'
import { invokeWebApiWrapperAsync } from './factory'
import { WellIcon, WellIconState } from './icons'
import '../styles/wellTreeSelect.css' import '../styles/wellTreeSelect.css'
export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown' export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown'

View File

@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from "react" import { notification } from 'antd'
import { notification } from 'antd'; import { Dispatch, SetStateAction } from 'react'
import { FileInfoDto } from '../services/api' import { FileInfoDto } from '../services/api'
const notificationTypeDictionary = new Map([ const notificationTypeDictionary = new Map([
@ -15,29 +15,28 @@ const notificationTypeDictionary = new Map([
* @param notifyType для параметра типа. Допустимые значение 'error', 'warning', 'info' * @param notifyType для параметра типа. Допустимые значение 'error', 'warning', 'info'
*/ */
export const notify = (body: string | any, notifyType: string = 'info', other?: any) => { export const notify = (body: string | any, notifyType: string = 'info', other?: any) => {
if(!body) if (!body) return
return
const instance = notificationTypeDictionary.get(notifyType) ?? const instance = notificationTypeDictionary.get(notifyType) ??
notificationTypeDictionary.get('open') notificationTypeDictionary.get('open')
instance?.notifyInstance({ instance?.notifyInstance({
description: body, description: body,
message: instance.caption, message: instance.caption,
placement: "bottomRight", placement: 'bottomRight',
duration: 10, duration: 10,
...other ...other
}) })
} }
type asyncFunction = (...args:any) => Promise<any|void>; type asyncFunction = (...args: any) => Promise<any|void>
export const invokeWebApiWrapperAsync = async ( export const invokeWebApiWrapperAsync = async (
funcAsync: asyncFunction, funcAsync: asyncFunction,
setShowLoader: Dispatch<SetStateAction<boolean>>, setShowLoader?: Dispatch<SetStateAction<boolean>>,
errorNotifyText: (string | ((ex: unknown) => string)) errorNotifyText?: (string | ((ex: unknown) => string))
) => { ) => {
if(setShowLoader) setShowLoader?.(true)
setShowLoader(true)
try{ try{
await funcAsync() await funcAsync()
} catch (ex) { } catch (ex) {
@ -49,48 +48,42 @@ export const invokeWebApiWrapperAsync = async (
else notify(errorNotifyText, 'error') else notify(errorNotifyText, 'error')
} }
} finally { } finally {
if(setShowLoader) setShowLoader?.(false)
setShowLoader(false)
} }
} }
export const download = async (url: string, fileName?: string) => { export const download = async (url: string, fileName?: string) => {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
Authorization: 'Bearer ' + localStorage['token'] Authorization: `Bearer ${localStorage['token']}`
}, },
method: 'Get' method: 'Get'
}) })
if(response.status !== 200) if(response.status !== 200)
throw response throw response
const requestFileName = decodeURI(fileName const requestFileName = decodeURI(
??response.headers fileName
.get('content-disposition') ?? response.headers.get('content-disposition')?.split(';').pop()?.split(`'`).pop()
?.split(';') ?? url.replace('\\','/').split('/').pop()
.splice(-1)[0] ?? 'file'
.split("'") )
.splice(-1)[0]
?? url.replace('\\','/')
.split('/')
.splice(-1)[0]
?? 'file')
const blob = await response.blob() const blob = await response.blob()
const reader = new FileReader(); const reader = new FileReader()
reader.readAsDataURL(blob); reader.readAsDataURL(blob)
reader.onload = function (e) { reader.onload = (e) => {
let a = document.createElement('a'); const a = document.createElement('a')
a.href = (e.target?.result?.toString() ?? ''); a.href = (e.target?.result?.toString() ?? '')
a.download = requestFileName; a.download = requestFileName
document.body.appendChild(a); // we need to append the element to the dom -> otherwise it will not work in firefox document.body.appendChild(a) // we need to append the element to the dom -> otherwise it will not work in firefox
a.click(); a.click()
a.remove(); a.remove()
}; }
} }
export const upload = async (url: string, formData: FormData) => { export const upload = async (url: string, formData: FormData) => {
let response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
Authorization: 'Bearer ' + localStorage['token'] Authorization: `Bearer ${localStorage['token']}`
}, },
method: 'Post', method: 'Post',
body: formData, body: formData,
@ -115,20 +108,3 @@ export const formatBytes = (bytes:number) => {
else else
return `${(bytes / 1024 / 1024).toFixed(2)}Mb` return `${(bytes / 1024 / 1024).toFixed(2)}Mb`
} }
export const formatTimespan = (seconds:number) => {
const days = Math.floor(seconds / 86400)
seconds = seconds % 86400
const hours = Math.floor(seconds / 3600)
seconds = seconds % 3600
const minutes = Math.floor(seconds / 60)
seconds = seconds % 60
let formatedTimespan = ''
if(days > 0)
formatedTimespan += days + ' '
formatedTimespan += hours.toString().padStart(2,'0') + ':' +
minutes.toString().padStart(2,'0') + ':' +
seconds.toString().padStart(2,'0')
return formatedTimespan
}

View File

@ -55,6 +55,13 @@ export default function ClusterWells({statsWells}) {
if (!filtersWellsType.some((el) => el.text === well.wellType)) if (!filtersWellsType.some((el) => el.text === well.wellType))
filtersWellsType.push({ text: well.wellType, value: well.wellType,}) filtersWellsType.push({ text: well.wellType, value: well.wellType,})
let periodPlanValue = well.total?.plan?.start && well.total?.plan?.end
? (new Date(well.total?.plan?.end) - new Date(well.total?.plan?.start)) / DAY_IN_MS
: '-'
let periodFactValue = well.total?.fact?.start && well.total?.fact?.end
? (new Date(well.total?.fact?.end) - new Date(well.total?.fact?.start)) / DAY_IN_MS
: '-'
return { return {
key: well.caption, key: well.caption,
id: well.id, id: well.id,
@ -62,8 +69,8 @@ export default function ClusterWells({statsWells}) {
wellType: well.wellType, wellType: well.wellType,
factStart: well.total?.fact?.start, factStart: well.total?.fact?.start,
factEnd: well.total?.fact?.end, factEnd: well.total?.fact?.end,
periodPlan: (new Date(well.total?.plan?.end) - new Date(well.total?.plan?.start)) / DAY_IN_MS, periodPlan: periodPlanValue,
periodFact: (new Date(well.total?.fact?.end) - new Date(well.total?.fact?.start)) / DAY_IN_MS, periodFact: periodFactValue,
rateOfPenetrationPlan: well.total?.plan?.rop, rateOfPenetrationPlan: well.total?.plan?.rop,
rateOfPenetrationFact: well.total?.fact?.rop, rateOfPenetrationFact: well.total?.fact?.rop,
routeSpeedPlan: well.total?.plan?.routeSpeed, routeSpeedPlan: well.total?.plan?.routeSpeed,
@ -91,7 +98,9 @@ export default function ClusterWells({statsWells}) {
setTableData(data) setTableData(data)
}, [statsWells]) }, [statsWells])
const getDate = (str) => Number.isNaN(+new Date(str)) ? '-' : new Date(str).toLocaleString() const getDate = (str) => Number.isNaN(+new Date(str)) || +new Date(str) === 0
? '-'
: new Date(str).toLocaleString()
const columns = [ const columns = [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
@ -110,11 +119,11 @@ export default function ClusterWells({statsWells}) {
makeTextColumn('Тип скв.', 'wellType', filtersWellsType, null, (text) => text ?? '-'), makeTextColumn('Тип скв.', 'wellType', filtersWellsType, null, (text) => text ?? '-'),
makeGroupColumn('Фактические сроки', [ makeGroupColumn('Фактические сроки', [
makeColumn('начало', 'factStart', { sorter: makeDateSorter('factStart'), render: getDate }), makeColumn('начало', 'factStart', { sorter: makeDateSorter('factStart'), render: getDate }),
makeColumn('окончание', 'factEnd', { sorter: makeDateSorter('factEnd'), render: getDate }) makeColumn('окончание', 'factEnd', { sorter: makeDateSorter('factEnd'), render: getDate }),
]), ]),
makeNumericColumnPlanFact('Продолжительность', 'period', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('МСП', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Рейсовая скорость', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('НПВ, сут', 'notProductiveTime', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('НПВ, сут', 'notProductiveTime', filtersMinMax, makeFilterMinMaxFunction),
{ {
title: 'TVD', title: 'TVD',

View File

@ -1,29 +1,38 @@
import React, { useEffect, useState } from 'react'
import { Modal } from 'antd' import { Modal } from 'antd'
import { memo, useEffect, useState } from 'react'
import { Table } from '../../components/Table' import { Table } from '../../components/Table'
import { formatDate } from './MeasureTable' import { formatDate } from '../../utils'
import { v } from './columnsCommon' import { v } from './columnsCommon'
export const InclinometryTable = React.memo(({ group, visible, onClose }) => { const tableScroll = { y: 400, x: 1300 }
const [tableColumns, setTableColumns] = useState([])
const [tableData, setTableData] = useState([])
useEffect(() => { const dateColumn = {
setTableColumns([{
title: 'Дата', title: 'Дата',
key: 'date', key: 'date',
dataIndex: 'date', dataIndex: 'date',
render: (item) => formatDate(item), render: (date) => formatDate(date, true) ?? 'Нет данных',
fixed: 'left', fixed: 'left',
width: '8rem', width: '8rem',
}, }
...(group?.columns?.map((column) => ({...column, title: v(column.title)})) ?? [])
])
}, [group?.columns])
useEffect(() => { export const InclinometryTable = memo(({ group, visible, onClose }) => {
setTableData(group?.values?.map(row => ({ date: row.timestamp, ...row.data }))) const [tableColumns, setTableColumns] = useState([])
}, [group?.values]) const [tableData, setTableData] = useState([])
useEffect(() => setTableColumns([
dateColumn,
...(group?.columns?.map((column) => ({
...column,
title: v(column.title)
})) ?? [])
]), [group?.columns])
useEffect(() => setTableData(group?.values?.map(row => ({
date: row.timestamp,
...row.data
}))), [group?.values])
return !group?.columns ? null : ( return !group?.columns ? null : (
<Modal <Modal
@ -37,14 +46,9 @@ export const InclinometryTable = React.memo(({ group, visible, onClose }) => {
<Table <Table
dataSource={tableData} dataSource={tableData}
columns={tableColumns} columns={tableColumns}
scroll={{ scroll={tableScroll}
y: 400,
x: 1300
}}
bordered bordered
/> />
</Modal> </Modal>
) )
}) })
export default InclinometryTable

View File

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Button, Form, Input, Popconfirm, Timeline } from 'antd' import { Button, Form, Input, Popconfirm, Timeline } from 'antd'
import moment from 'moment'
import { import {
CheckSquareOutlined, CheckSquareOutlined,
EditOutlined, EditOutlined,
@ -9,15 +8,19 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
DeleteOutlined DeleteOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { View } from './View'
import LoaderPortal from '../../components/LoaderPortal' import LoaderPortal from '../../components/LoaderPortal'
import { MeasureService } from '../../services/api' import { MeasureService } from '../../services/api'
import '../../styles/index.css'
import '../../styles/measure.css'
import { invokeWebApiWrapperAsync } from '../../components/factory' import { invokeWebApiWrapperAsync } from '../../components/factory'
const dateFormat = 'YYYY.MM.DD HH:mm' import { View } from './View'
export const formatDate = (date) => date ? moment.utc(date).local().format(dateFormat) : 'Нет данных'
import '../../styles/index.css'
import '../../styles/measure.css'
import { formatDate } from '../../utils'
const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => ({ render: renderDelegate, ...col }))
export const MeasureTable = ({idWell, group, updateMeasuresFunc, additionalButtons}) => { export const MeasureTable = ({idWell, group, updateMeasuresFunc, additionalButtons}) => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -29,11 +32,10 @@ export const MeasureTable = ({idWell, group, updateMeasuresFunc, additionalButto
const [measuresForm] = Form.useForm() const [measuresForm] = Form.useForm()
const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => ({ render: renderDelegate, ...col }))
useEffect(() => { useEffect(() => {
const data = [group.defaultValue].concat(group.values ?? []) let data = [group.defaultValue]
if (group?.values?.length > 0)
data = group.values
setData(data) setData(data)
setDisplayedValues(data.at(-1)) setDisplayedValues(data.at(-1))
}, [group.defaultValue, group.values]) }, [group.defaultValue, group.values])
@ -136,12 +138,12 @@ export const MeasureTable = ({idWell, group, updateMeasuresFunc, additionalButto
key={index} key={index}
className={'measure-button'} className={'measure-button'}
onClick={() => setDisplayedValues(item)} onClick={() => setDisplayedValues(item)}
dot={item?.id !== displayedValues?.id ? null : dot={item?.id === displayedValues?.id &&
<CheckSquareOutlined className={'timeline-clock-icon'} /> <CheckSquareOutlined className={'timeline-clock-icon'} />
} }
> >
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : ''}> <span className={item?.id === displayedValues?.id ? 'selected-timeline' : undefined}>
{formatDate(item.timestamp)} {formatDate(item.timestamp, true) ?? 'Нет данных'}
</span> </span>
</Timeline.Item> </Timeline.Item>
)} )}

View File

@ -1,36 +1,24 @@
import React from 'react' import { memo, Fragment } from 'react'
import { Empty, Form } from 'antd' import { Empty, Form } from 'antd'
import { Grid, GridItem } from '../../components/Grid' import { Grid, GridItem } from '../../components/Grid'
import '../../styles/index.css' import '../../styles/index.css'
import '../../styles/measure.css'
const colsCount = 3 const colsCount = 3
const headerCellStyle = { export const View = memo(({ columns, item }) => !item || !columns?.length ? (
border:'1px solid lightgrey' <Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
} ) : (
const valueCellStyle = {
border:'1px solid lightgrey',
justifyContent:'right',
marginRight:'16px',
fontWeight:'bold',
textAlign:'right',
padding: 0
}
export const View = React.memo(({columns, item}) => {
if (!item || !columns?.length)
return <Empty key='empty' image={Empty.PRESENTED_IMAGE_SIMPLE}/>
return (
<Grid> <Grid>
{columns.map((column, i) => ( {columns.map((column, i) => (
<> <Fragment key={i}>
<GridItem <GridItem
key={column.dataIndex} key={column.dataIndex}
row={Math.floor(i / colsCount) + 1} row={Math.floor(i / colsCount) + 1}
col={(i % colsCount) * 2 + 1} col={(i % colsCount) * 2 + 1}
style={headerCellStyle} className={'measure-column-header'}
> >
{column.title} {column.title}
</GridItem> </GridItem>
@ -39,23 +27,24 @@ export const View = React.memo(({columns, item}) => {
key={column.title} key={column.title}
row={Math.floor(i / colsCount) + 1} row={Math.floor(i / colsCount) + 1}
col={(i % colsCount) * 2 + 2} col={(i % colsCount) * 2 + 2}
style={valueCellStyle} className={'measure-column-value'}
style={{ padding: 0 }}
> >
{column.render ? ( {column.render ? (
<Form.Item <Form.Item
key={column.dataIndex} key={column.dataIndex}
name={column.dataIndex} name={column.dataIndex}
style={{ margin: 0 }} style={{ margin: 0 }}
//rules={column.formItemRules}
> >
{column.render(item[column.dataIndex])} {column.render(item[column.dataIndex])}
</Form.Item> </Form.Item>
) : ( ) : (
<p key={column.title} className='m-5px'>{item[column.dataIndex]}</p> <p key={column.title} className={'m-5px'}>
{item[column.dataIndex]}
</p>
)} )}
</GridItem> </GridItem>
</> </Fragment>
))} ))}
</Grid> </Grid>
) ))
})

View File

@ -1,27 +1,12 @@
import React from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { RegExpIsFloat } from '../../components/Table' import { RegExpIsFloat } from '../../components/Table'
const { TextArea } = Input import '../../styles/measure.css'
export const v = (text) => ( export const v = (text) => (
<div style={{ <div className={'v-div'}>
display: 'flex', <span className={'v-span'}>
height: '180px',
justifyContent: 'center'
}}>
<span
style={{
whiteSpace: 'pre',
verticalAlign: 'center',
textAlign: 'center',
WebkitTransform: 'rotate(-90deg)',
MozTransform: 'rotate(-90deg)',
MsTransform: 'rotate(-90deg)',
OTransform: 'rotate(-90deg)',
transform: 'rotate(-90deg)',
}}>
{text} {text}
</span> </span>
</div> </div>
@ -42,7 +27,7 @@ export const numericColumnOptions = {
export const textColumnOptions = { export const textColumnOptions = {
editable: true, editable: true,
input: <TextArea/>, input: <Input.TextArea/>,
width: '3rem', width: '3rem',
formItemRules: [{ formItemRules: [{
required: true, required: true,

View File

@ -1,4 +1,5 @@
import { makeColumn } from '../../components/Table'; import { makeColumn } from '../../components/Table'
import { numericColumnOptions, textColumnOptions } from './columnsCommon' import { numericColumnOptions, textColumnOptions } from './columnsCommon'
export const columnsDrillingFluid = [ export const columnsDrillingFluid = [
@ -30,7 +31,7 @@ export const columnsDrillingFluid = [
makeColumn('Твердая фаза раствора, %', 'fluidSolidPhase', numericColumnOptions), makeColumn('Твердая фаза раствора, %', 'fluidSolidPhase', numericColumnOptions),
makeColumn('Смазка, %', 'grease', numericColumnOptions), makeColumn('Смазка, %', 'grease', numericColumnOptions),
makeColumn('Карбонат кальция, кг/м³', 'calciumCarbonate', numericColumnOptions), makeColumn('Карбонат кальция, кг/м³', 'calciumCarbonate', numericColumnOptions),
]; ]
export const drillingFluidDefaultData = { export const drillingFluidDefaultData = {
idWell: 0, idWell: 0,
@ -38,33 +39,33 @@ export const drillingFluidDefaultData = {
idCategory: 0, idCategory: 0,
isDefaultData: true, isDefaultData: true,
data: { data: {
'name': 0, name: 0,
'temperature': 0, temperature: 0,
'density': 0, density: 0,
'conditionalViscosity': 0, conditionalViscosity: 0,
'r300': 0, r300: 0,
'r600': 0, r600: 0,
'r3r6': 0, r3r6: 0,
'dnsDpa': 0, dnsDpa: 0,
'plasticViscocity': 0, plasticViscocity: 0,
'snsDpa': 0, snsDpa: 0,
'r3r649С': 0, r3r649С: 0,
'dns49Cdpa': 0, dns49Cdpa: 0,
'plasticViscocity49c': 0, plasticViscocity49c: 0,
'sns49Cdpa': 0, sns49Cdpa: 0,
'mbt': 0, mbt: 0,
'sand': 0, sand: 0,
'filtering': 0, filtering: 0,
'crust': 0, crust: 0,
'ktk': 0, ktk: 0,
'ph': 0, ph: 0,
'hardness': 0, hardness: 0,
'chlorides': 0, chlorides: 0,
'pf': 0, pf: 0,
'mf': 0, mf: 0,
'pm': 0, pm: 0,
'fluidSolidPhase': 0, fluidSolidPhase: 0,
'grease': 0, grease: 0,
'calciumCarbonate': 0 calciumCarbonate: 0
} }
} }

View File

@ -1,47 +1,55 @@
import { Button } from 'antd'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { TableOutlined } from '@ant-design/icons'
import { MeasureService } from '../../services/api'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { MeasureTable } from './MeasureTable'
import { InclinometryTable } from './InclinometryTable'
import { columnsNnb, nnbDefaultData } from './nnbData' import { columnsNnb, nnbDefaultData } from './nnbData'
import { columnsMudDiagram, mudDiagramDefaultData } from './mudDiagramData' import { columnsMudDiagram, mudDiagramDefaultData } from './mudDiagramData'
import { columnsDrillingFluid, drillingFluidDefaultData } from './drillingFluidData' import { columnsDrillingFluid, drillingFluidDefaultData } from './drillingFluidData'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { MeasureService } from '../../services/api'
import LoaderPortal from '../../components/LoaderPortal'
import { MeasureTable } from './MeasureTable'
import InclinometryTable from './InclinometryTable'
import { Button } from 'antd'
import { TableOutlined } from '@ant-design/icons'
const defaultData = [ const defaultData = [
{ {
idCategory: 1, idCategory: 1,
title: 'Замер бурового раствора', title: 'Замер бурового раствора',
columns: columnsDrillingFluid, columns: columnsDrillingFluid,
values: [drillingFluidDefaultData],
defaultValue: drillingFluidDefaultData, defaultValue: drillingFluidDefaultData,
}, }, {
{
idCategory: 2, idCategory: 2,
title: 'Шламограмма', title: 'Шламограмма',
columns: columnsMudDiagram, columns: columnsMudDiagram,
values: [mudDiagramDefaultData],
defaultValue: mudDiagramDefaultData, defaultValue: mudDiagramDefaultData,
}, }, {
{
idCategory: 3, idCategory: 3,
title: 'ННБ', title: 'ННБ',
columns: columnsNnb, columns: columnsNnb,
values: [nnbDefaultData],
defaultValue: nnbDefaultData, defaultValue: nnbDefaultData,
additionalButtons: (group, idx, onClick) => (isEditing) => isEditing ? null : (
<Button
key={'table'}
className={'flex-1'}
onClick={() => onClick(idx)}
disabled={!group.values || group.values.length <= 0}
>
<TableOutlined />
</Button>
)
} }
] ]
export default function Measure({idWell}){ export const Measure = ({ idWell }) => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(false) const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true)
const [data, setData] = useState(defaultData) const [data, setData] = useState(defaultData)
const [tableIdx, setTableIdx] = useState(-1) const [tableIdx, setTableIdx] = useState(-1)
const updateCurrentValues = () => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
if (!isMeasuresUpdating) return
const measures = await MeasureService.getHisory(idWell) const measures = await MeasureService.getHisory(idWell)
setIsMeasuresUpdating(false) setIsMeasuresUpdating(false)
@ -57,28 +65,9 @@ export default function Measure({idWell}){
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить последние данные по скважине ${idWell}` `Не удалось загрузить последние данные по скважине ${idWell}`
) ), [idWell, isMeasuresUpdating])
useEffect(() => { return (
setData(prevData => {
prevData[2].additionalButtons = (group, idx) => (isEditing) => isEditing ? null : (
<Button
key={'table'}
className={'flex-1'}
onClick={() => setTableIdx(idx)}
disabled={!group.values || group.values.length <= 0}
>
<TableOutlined />
</Button>
)
return prevData
})
}, [])
useEffect(updateCurrentValues, [idWell, isMeasuresUpdating])
return <>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
{data.map((group, idx) => ( {data.map((group, idx) => (
<MeasureTable <MeasureTable
@ -86,7 +75,7 @@ export default function Measure({idWell}){
idWell={idWell} idWell={idWell}
group={group} group={group}
updateMeasuresFunc={() => setIsMeasuresUpdating(true)} updateMeasuresFunc={() => setIsMeasuresUpdating(true)}
additionalButtons={group.additionalButtons?.(group, idx)} additionalButtons={group.additionalButtons?.(group, idx, setTableIdx)}
/> />
))} ))}
<InclinometryTable <InclinometryTable
@ -95,5 +84,7 @@ export default function Measure({idWell}){
group={data[tableIdx]} group={data[tableIdx]}
/> />
</LoaderPortal> </LoaderPortal>
</> )
} }
export default Measure

View File

@ -32,27 +32,27 @@ export const mudDiagramDefaultData = {
idCategory: 0, idCategory: 0,
isDefaultData: true, isDefaultData: true,
data: { data: {
'probeNumber': 0, probeNumber: 0,
'probeExtractionDepth': 0, probeExtractionDepth: 0,
'sandstone': 0, sandstone: 0,
'siltstone': 0, siltstone: 0,
'argillit': 0, argillit: 0,
'brokenArgillit': 0, brokenArgillit: 0,
'coal': 0, coal: 0,
'sand': 0, sand: 0,
'clay': 0, clay: 0,
'camstone': 0, camstone: 0,
'cement': 0, cement: 0,
'summary': '-', summary: '-',
'drillingMud': 0, drillingMud: 0,
'sludge': 0, sludge: 0,
'maxSum': 0, maxSum: 0,
'methane': 0, methane: 0,
'ethane': 0, ethane: 0,
'propane': 0, propane: 0,
'butane': 0, butane: 0,
'pentane': 0, pentane: 0,
'mechanicalSpeed': 0, mechanicalSpeed: 0,
'preliminaryConclusion': '-' preliminaryConclusion: '-'
} }
} }

View File

@ -19,7 +19,7 @@ export const columnsNnb = [
makeColumn('Комментарий', 'comment', numericColumnOptions), makeColumn('Комментарий', 'comment', numericColumnOptions),
makeColumn('Разница вертикальных глубин\nмежду планом и фактом', 'depthPlanFactDifference', numericColumnOptions), makeColumn('Разница вертикальных глубин\nмежду планом и фактом', 'depthPlanFactDifference', numericColumnOptions),
makeColumn('Расстояние в пространстве\nмежду планом и фактом', 'distancePlanFactDifference', numericColumnOptions), makeColumn('Расстояние в пространстве\nмежду планом и фактом', 'distancePlanFactDifference', numericColumnOptions),
]; ]
export const nnbDefaultData = { export const nnbDefaultData = {
idWell: 0, idWell: 0,
@ -27,22 +27,22 @@ export const nnbDefaultData = {
idCategory: 0, idCategory: 0,
isDefaultData: true, isDefaultData: true,
data: { data: {
'depth': 0, depth: 0,
'zenithAngle': 0, zenithAngle: 0,
'magneticAzimuth': 0, magneticAzimuth: 0,
'trueAzimuth': 0, trueAzimuth: 0,
'directAzimuth': 0, directAzimuth: 0,
'verticalDepth': 0, verticalDepth: 0,
'absoluteMark': 0, absoluteMark: 0,
'localNorthOffset': 0, localNorthOffset: 0,
'localEastOffset': 0, localEastOffset: 0,
'outFallOffset': 0, outFallOffset: 0,
'offsetAzimuth': 0, offsetAzimuth: 0,
'areaIntensity': '-', areaIntensity: '-',
'offsetStopAngle': 0, offsetStopAngle: 0,
'zenithIntensity': 0, zenithIntensity: 0,
'comment': '-', comment: '-',
'depthPlanFactDifference': 0, depthPlanFactDifference: 0,
'distancePlanFactDifference': 0 distancePlanFactDifference: 0
} }
} }

View File

@ -1,62 +1,57 @@
import { useState, useEffect } from "react" import { Button, Tooltip } from 'antd'
import { Table, formatDate, makeDateSorter, makeNumericSorter} from "../../components/Table" import { useState, useEffect } from 'react'
import { Button, Tooltip } from "antd"
import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons' import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { ReportService } from "../../services/api"
import { invokeWebApiWrapperAsync, downloadFile, formatTimespan} from "../../components/factory" import { ReportService } from '../../services/api'
import LoaderPortal from "../../components/LoaderPortal" import { formatDate, periodToString } from '../../utils'
import moment from "moment" import LoaderPortal from '../../components/LoaderPortal'
import { Table, makeDateSorter, makeNumericSorter } from '../../components/Table'
import { invokeWebApiWrapperAsync, downloadFile } from '../../components/factory'
const imgPaths = { const imgPaths = {
".pdf": <FilePdfOutlined/>, '.pdf': <FilePdfOutlined/>,
".las": <FileTextOutlined />, '.las': <FileTextOutlined/>,
} }
const columns = [ const columns = [
{ {
title: "Название", title: 'Название',
dataIndex: "name", dataIndex: 'name',
key: "name", key: 'name',
render: (name, report) => ( render: (name, report) => (
<Button <Button
type="link" type={'link'}
icon={imgPaths[report.format]} icon={imgPaths[report.format]}
onClick={(_) => downloadFile(report.file)} onClick={() => downloadFile(report.file)}
download={name} download={name}
> >
{name} {name}
</Button> </Button>
), ),
}, }, {
{ title: <Tooltip title={'Дата формирования'}>Сформирован</Tooltip>,
title: <Tooltip title="Дата формирования">Сформирован</Tooltip>, dataIndex: 'date',
dataIndex: "date", key: 'date',
key: "date",
sorter: makeDateSorter('date'), sorter: makeDateSorter('date'),
render: date=>moment(date).format(formatDate), render: (date) => formatDate(date),
}, }, {
{ title: <Tooltip title={'Дата начала периода рапорта'}>С</Tooltip>,
title: <Tooltip title="Дата начала периода рапорта">С</Tooltip>, dataIndex: 'begin',
dataIndex: "begin", key: 'begin',
key: "begin",
sorter: makeDateSorter('begin'), sorter: makeDateSorter('begin'),
render: date=>moment(date).format(formatDate), render: (date) => formatDate(date),
}, }, {
{ title: <Tooltip title={'Дата окончания периода рапорта'}>По</Tooltip>,
title: <Tooltip title="Дата окончания периода рапорта">По</Tooltip>, dataIndex: 'end',
dataIndex: "end", key: 'end',
key: "end",
sorter: makeDateSorter('end'), sorter: makeDateSorter('end'),
render: date=>moment(date).format(formatDate), render: (date) => formatDate(date),
}, }, {
{ title: <Tooltip title={'шаг сетки графиков'}>шаг, сек</Tooltip>,
title: <Tooltip title="шаг сетки графиков">шаг, сек</Tooltip>, dataIndex: 'step',
dataIndex: "step", key: 'step',
key: "step",
sorter: makeNumericSorter('step'), sorter: makeNumericSorter('step'),
render: step => periodToString(step),
render: step => formatTimespan(step)
}, },
] ]
@ -64,18 +59,25 @@ export const Reports = ({idWell}) => {
const [reports, setReports] = useState([]) const [reports, setReports] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
useEffect(()=>invokeWebApiWrapperAsync(async()=>{ useEffect(() => invokeWebApiWrapperAsync(
async () => {
const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell) const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell)
const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date })) const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date }))
setReports(reports) setReports(reports)
},setShowLoader), [idWell]) },
setShowLoader,
`Не удалось загрузить список рапортов по скважине "${idWell}"`
), [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Table <Table
size='small' bordered size={'small'}
bordered
columns={columns} columns={columns}
dataSource={reports} dataSource={reports}
pagination={{pageSize:13}}/> pagination={{ pageSize: 13 }}
</LoaderPortal>) />
</LoaderPortal>
)
} }

View File

@ -1,63 +1,55 @@
import { useState, useEffect } from "react" import 'moment/locale/ru'
import { DatePicker, Radio, Button, Select, notification } from "antd" import moment from 'moment'
import "moment/locale/ru" import { useState, useEffect } from 'react'
import moment from "moment" import { DatePicker, Radio, Button, Select, notification } from 'antd'
import { Subscribe } from "../../services/signalr"
import LoaderPortal from "../../components/LoaderPortal"
import { ReportService } from "../../services/api"
import { ReportCreationNotify } from "./ReportCreationNotify"
import { invokeWebApiWrapperAsync, notify } from "../../components/factory"
import { Reports } from "./Reports"
const { RangePicker } = DatePicker import { ReportService } from '../../services/api'
const { Option } = Select import { Subscribe } from '../../services/signalr'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { Reports } from './Reports'
import { ReportCreationNotify } from './ReportCreationNotify'
const timePeriodNames = [ const timePeriodNames = [
{ label: "1 секунда", value: 1 }, { label: '1 секунда', value: 1 },
{ label: "10 секунд", value: 10 }, { label: '10 секунд', value: 10 },
{ label: "1 минута", value: 60 }, { label: '1 минута', value: 60 },
{ label: "5 минут", value: 300 }, { label: '5 минут', value: 300 },
{ label: "30 минут", value: 1800 }, { label: '30 минут', value: 1800 },
{ label: "1 час", value: 3600 }, { label: '1 час', value: 3600 },
{ label: "6 часов", value: 21600 }, { label: '6 часов', value: 21600 },
{ label: "12 часов", value: 43200 }, { label: '12 часов', value: 43200 },
{ label: "1 день", value: 86400 }, { label: '1 день', value: 86400 },
{ label: "1 неделя", value: 604800 }, { label: '1 неделя', value: 604800 },
] ]
const dateTimeFormat = "DD.MM.YYYY hh:mm:ss" const dateTimeFormat = 'DD.MM.YYYY hh:mm:ss'
const reportFormats = [
{ value: 0, label: 'PDF' },
{ value: 1, label: 'LAS' },
]
export default function Report({ idWell }) { export default function Report({ idWell }) {
const [aviableDateRange, setAviableDateRange] = useState([ const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()])
moment(),
moment(),
])
const [filterDateRange, setFilterDateRange] = useState([ const [filterDateRange, setFilterDateRange] = useState([
moment().subtract(1, "days").startOf("day"), moment().subtract(1, 'days').startOf('day'),
moment().startOf("day"), moment().startOf('day'),
]) ])
const [step, setStep] = useState(timePeriodNames[2].value) const [step, setStep] = useState(timePeriodNames[2].value)
const [format, setFormat] = useState(0) const [format, setFormat] = useState(0)
const [pagesCount, setPagesCount] = useState(0) const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const periodOptions = timePeriodNames.map((item) => ( const handleReportCreation = async () => await invokeWebApiWrapperAsync(
<Option key={item.value} value={item.value}> async () => {
{item.label}
</Option>
))
const handleReportCreation = async () => {
let begin = filterDateRange[0].toISOString()
let end = filterDateRange[1].toISOString()
try {
const taskId = await ReportService.createReport( const taskId = await ReportService.createReport(
idWell, idWell,
step, step,
format, format,
begin, filterDateRange[0].toISOString(),
end filterDateRange[1].toISOString()
) )
if (!taskId) return if (!taskId) return
@ -65,12 +57,8 @@ export default function Report({ idWell }) {
if (progressData) { if (progressData) {
notification.open({ notification.open({
key: taskId, key: taskId,
message: "Создание отчета:", message: 'Создание отчета:',
description: ( description: <ReportCreationNotify progressData={progressData} />,
<ReportCreationNotify
progressData={progressData}
></ReportCreationNotify>
),
duration: 0, duration: 0,
onClose: unSubscribeReportHub, onClose: unSubscribeReportHub,
}) })
@ -81,64 +69,52 @@ export default function Report({ idWell }) {
} }
const unSubscribeReportHub = Subscribe( const unSubscribeReportHub = Subscribe(
"hubs/reports", 'hubs/reports',
"GetReportProgress", 'GetReportProgress',
`Report_${taskId}`, `Report_${taskId}`,
handleReportProgress handleReportProgress
) )
} catch (error) { },
notify( setShowLoader,
`Не удалось создать отчет по скважине (${idWell}) c `Не удалось создать отчет по скважине (${idWell}) c
${filterDateRange[0].format("DD.MM.YYYY hh:mm:ss")} по ${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format("DD.MM.YYYY hh:mm:ss")}`, ${filterDateRange[1].format(dateTimeFormat)}`
"error"
) )
console.log(error)
}
}
const disabledDate = (current) => { const disabledDate = (current) => !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
return current < aviableDateRange[0] || current > aviableDateRange[1]
} useEffect(() => invokeWebApiWrapperAsync(
async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(idWell)
if (!datesRangeResponse?.from || !datesRangeResponse.to)
throw new Error('Формат ответа неверный!')
useEffect(
() =>
invokeWebApiWrapperAsync(async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(
idWell
)
const datesRange = [ const datesRange = [
moment(datesRangeResponse.from), moment(datesRangeResponse.from),
moment(datesRangeResponse.to), moment(datesRangeResponse.to),
] ]
setAviableDateRange(datesRange) setAviableDateRange(datesRange)
let from = moment(datesRangeResponse.to) const from = moment.max(moment(datesRange[1]).subtract(1, 'days'), datesRange[0])
from = from.subtract(1, "days")
if (from < datesRange[0]) from = datesRange[0]
const filterDateDefaults = [ setFilterDateRange([
from.startOf("day"), from.startOf('day'),
moment(datesRangeResponse.to).startOf("day"), moment(datesRange[1]).startOf('day'),
] ])
setFilterDateRange(filterDateDefaults) },
}, setShowLoader), setShowLoader,
[idWell] `Не удалось получить диапозон дат рапортов для скважины "${idWell}"`
) ), [idWell])
useEffect( useEffect(() => invokeWebApiWrapperAsync(
() =>
invokeWebApiWrapperAsync(
async () => { async () => {
if (!filterDateRange || filterDateRange.length < 2) return if (filterDateRange?.length !== 2) return
const begin = filterDateRange[0].toISOString()
const end = filterDateRange[1].toISOString()
const pagesCount = await ReportService.getReportSize( const pagesCount = await ReportService.getReportSize(
idWell, idWell,
step, step,
format, format,
begin, filterDateRange[0].toISOString(),
end filterDateRange[1].toISOString()
) )
setPagesCount(pagesCount) setPagesCount(pagesCount)
}, },
@ -146,17 +122,15 @@ export default function Report({ idWell }) {
`Не удалось получить предварительные параметры отчета c `Не удалось получить предварительные параметры отчета c
${filterDateRange[0].format(dateTimeFormat)} по ${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format(dateTimeFormat)}` ${filterDateRange[1].format(dateTimeFormat)}`
), ), [filterDateRange, step, format, idWell])
[filterDateRange, step, format, idWell]
)
return ( return (
<div> <div>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<div className="w-100 mt-20px mb-20px d-flex"> <div className={'w-100 mt-20px mb-20px d-flex'}>
<div> <div>
<div>Диапазон дат отчета</div> <div>Диапазон дат отчета</div>
<RangePicker <DatePicker.RangePicker
disabledDate={disabledDate} disabledDate={disabledDate}
allowClear={false} allowClear={false}
onCalendarChange={setFilterDateRange} onCalendarChange={setFilterDateRange}
@ -164,38 +138,28 @@ export default function Report({ idWell }) {
value={filterDateRange} value={filterDateRange}
/> />
</div> </div>
<div className="ml-30px"> <div className={'ml-30px'}>
<div>Шаг графиков</div> <div>Шаг графиков</div>
<Select style={{ width: "8rem" }} <Select
onChange={e=>{ style={{ width: '8rem' }}
setStep(e) onChange={e => setStep(e)}
}} value={step}
value={step}> options={timePeriodNames}
{periodOptions} />
</Select>
</div> </div>
<div className="ml-30px"> <div className={'ml-30px'}>
<div>Формат отчета</div> <div>Формат отчета</div>
<Radio.Group <Radio.Group
onChange={(e) => setFormat(e.target.value)} onChange={(e) => setFormat(e.target.value)}
options={reportFormats}
value={format} value={format}
> />
<Radio.Button value={0}>PDF</Radio.Button>
<Radio.Button value={1}>LAS</Radio.Button>
</Radio.Group>
</div> </div>
<div className="ml-30px"> <div className={'ml-30px'}>
<div>&nbsp;</div> <div>&nbsp;</div>
<Button type="primary" onClick={handleReportCreation}> <Button type={'primary'} onClick={handleReportCreation}>
<span>Получить рапорт</span> Получить рапорт ({pagesCount} стр.) {pagesCount > 100 && '!!!'}
<span className={"ml-5px"}>({pagesCount} стр.)</span>
<span
style={{ display: pagesCount > 100 ? "inline" : "none" }}
className={"ml-5px"}
>
!!!
</span>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
import moment from 'moment'
import { memo } from 'react' import { memo } from 'react'
import { Modal, Input } from 'antd' import { Modal, Input } from 'antd'
import { Table } from '../../components/Table' import { Table } from '../../components/Table'
import { UserView } from '../../components/Views' import { UserView } from '../../components/Views'
import { Grid, GridItem } from '../../components/Grid' import { Grid, GridItem } from '../../components/Grid'
import { periodToString } from '../../utils/datetime' import { formatDate, periodToString } from '../../utils'
export const setpointStatus = { export const setpointStatus = {
0: 'Неизвестно', 0: 'Неизвестно',
@ -16,9 +16,9 @@ export const setpointStatus = {
} }
export const getSetpointStatus = (id) => { export const getSetpointStatus = (id) => {
if (!id || Object.keys(setpointStatus).every((idx) => Number(idx) !== id)) if (id && Object.keys(setpointStatus).some((idx) => Number(idx) === id))
return setpointStatus[0]
return setpointStatus[id] return setpointStatus[id]
return setpointStatus[0]
} }
const columns = [ const columns = [
@ -27,14 +27,11 @@ const columns = [
] ]
export const SetpointViewer = memo(({ setpoint, visible, onClose, setpointNames }) => { export const SetpointViewer = memo(({ setpoint, visible, onClose, setpointNames }) => {
let date = moment(setpoint?.uploadDate).format('DD MMM YYYY, HH:mm:ss') const date = formatDate(setpoint?.uploadDate)
let setpoints = [] const setpoints = Object.keys(setpoint?.setpoints ?? []).map((name) => ({
if (setpoint) {
setpoints = Object.keys(setpoint?.setpoints).map((name) => ({
name: setpointNames.find(spName => spName.value === name)?.label, name: setpointNames.find(spName => spName.value === name)?.label,
value: setpoint.setpoints[name] value: setpoint?.setpoints?.[name]
})) }))
}
return ( return (
<Modal <Modal
@ -55,7 +52,7 @@ export const SetpointViewer = memo(({ setpoint, visible, onClose, setpointNames
<GridItem row={3} col={2}>{getSetpointStatus(setpoint?.idState)}</GridItem> <GridItem row={3} col={2}>{getSetpointStatus(setpoint?.idState)}</GridItem>
<GridItem row={4} col={1}>Период актуальности рекомендаций:</GridItem> <GridItem row={4} col={1}>Период актуальности рекомендаций:</GridItem>
<GridItem row={4} col={2}>{periodToString(setpoint?.obsolescenceSec)}</GridItem> <GridItem row={4} col={2}>{periodToString(setpoint?.obsolescenceSec * 1000)}</GridItem>
<GridItem row={5} col={1}>Комментарий:</GridItem> <GridItem row={5} col={1}>Комментарий:</GridItem>
<GridItem row={6} col={1} colSpan={3}> <GridItem row={6} col={1} colSpan={3}>

View File

@ -122,13 +122,13 @@ export const WellCompositeSections = ({idWell, statsWells, selectedSections}) =>
(text, item) => <Link to={`/well/${item?.id}`}>{text ?? '-'}</Link> (text, item) => <Link to={`/well/${item?.id}`}>{text ?? '-'}</Link>
), ),
makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'), makeTextColumn('Секция', 'sectionType', filtersSectionsType, null, (text) => text ?? '-'),
makeNumericColumnPlanFact('Глубина', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Продолжительность', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Продолжительность, ч', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('МСП', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('МСП, м/ч', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Рейсовая скорость', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Спуск КНБК', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Подъем КНБК', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('Скорость спуска ОК', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFact('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFact('НПВ, сут', 'nonProductiveTime', filtersMinMax, makeFilterMinMaxFunction, null, '70px'), makeNumericColumnPlanFact('НПВ, сут', 'nonProductiveTime', filtersMinMax, makeFilterMinMaxFunction, null, '70px'),
{ {
title: 'TVD', title: 'TVD',

View File

@ -1,13 +1,11 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { import { DrillParamsService } from '../../services/api'
makeColumn,
makeNumericAvgRange
} from '../../components/Table'
import LoaderPortal from '../../components/LoaderPortal' import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory' import { invokeWebApiWrapperAsync } from '../../components/factory'
import { EditableTable, SelectFromDictionary } from '../../components/Table' import { EditableTable, SelectFromDictionary } from '../../components/Table'
import { DrillParamsService } from '../../services/api' import { makeActionHandler, makeColumn, makeNumericAvgRange } from '../../components/Table'
import { dictionarySectionType, getByKeyOrReturnKey } from './dictionary' import { dictionarySectionType, getByKeyOrReturnKey } from './dictionary'
export const columns = [ export const columns = [
@ -17,12 +15,11 @@ export const columns = [
width: 160, width: 160,
render: (_, record) => getByKeyOrReturnKey(dictionarySectionType, record.idWellSectionType) render: (_, record) => getByKeyOrReturnKey(dictionarySectionType, record.idWellSectionType)
}), }),
// makeNumericStartEnd('Глубина', 'depth'), makeNumericAvgRange('Нагрузка, т', 'axialLoad', 1),
makeNumericAvgRange('Нагрузка', 'axialLoad', 1), makeNumericAvgRange('Давление, атм', 'pressure', 1),
makeNumericAvgRange('Давление', 'pressure', 1), makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', 1),
makeNumericAvgRange('Момент на ВСП', 'rotorTorque', 1), makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed', 1),
makeNumericAvgRange('Обороты на ВСП', 'rotorSpeed', 1), makeNumericAvgRange('Расход, л/с', 'flow', 1)
makeNumericAvgRange('Расход', 'flow', 1)
] ]
export const WellDrillParams = ({ idWell }) => { export const WellDrillParams = ({ idWell }) => {
@ -40,24 +37,15 @@ export const WellDrillParams = ({idWell}) => {
useEffect(updateParams, [idWell]) useEffect(updateParams, [idWell])
const onAdd = async (param) => { const handlerProps = {
param.idWell = idWell service: DrillParamsService,
await DrillParamsService.insert(idWell, param) setLoader: setShowLoader,
updateParams() errorMsg: `Не удалось выполнить операцию`,
onComplete: updateParams,
idWell
} }
const onEdit = async (param) => { const recordParser = (record) => ({ ...record, idWell })
if (!param.id) return
param.idWell = idWell
await DrillParamsService.update(idWell, param.id, param)
updateParams()
}
const onDelete = async (param) => {
if (!param.id) return
await DrillParamsService.delete(idWell, param.id)
updateParams()
}
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -66,9 +54,9 @@ export const WellDrillParams = ({idWell}) => {
bordered bordered
columns={columns} columns={columns}
dataSource={params} dataSource={params}
onRowAdd={onAdd} onRowAdd={makeActionHandler('insert', handlerProps, recordParser)}
onRowEdit={onEdit} onRowEdit={makeActionHandler('update', handlerProps, recordParser)}
onRowDelete={onDelete} onRowDelete={makeActionHandler('delete', handlerProps, recordParser)}
pagination={false} pagination={false}
/> />
</LoaderPortal> </LoaderPortal>

View File

@ -1,37 +1,71 @@
import { useState, useEffect } from 'react'
import { Input } from 'antd'
import moment from 'moment' import moment from 'moment'
import { Input } from 'antd'
import { useState, useEffect } from 'react'
import { import {
EditableTable, EditableTable,
DatePickerWrapper, DatePickerWrapper,
SelectFromDictionary,
makeColumn, makeColumn,
makeDateSorter, makeDateSorter,
makeNumericColumnOptions} from "../../components/Table" makeNumericColumnOptions,
makeSelectColumn,
makeActionHandler,
} from '../../components/Table'
import LoaderPortal from '../../components/LoaderPortal' import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory' import { invokeWebApiWrapperAsync } from '../../components/factory'
import { WellOperationService} from '../../services/api' import { WellOperationService} from '../../services/api'
import { dictionarySectionType, getByKeyOrReturnKey } from './dictionary' import { formatDate } from '../../utils'
const { TextArea } = Input; const { TextArea } = Input
const basePageSize = 160; const basePageSize = 160
const format='YYYY.MM.DD HH:mm'
const defaultColumns = [
makeSelectColumn('Конструкция секции', 'idWellSectionType', [], undefined, { editable: true, width: 160 }),
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', {
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/> }),
]
export const WellOperationsEditor = ({idWell, idType}) => { export const WellOperationsEditor = ({idWell, idType}) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({current:1, pageSize:basePageSize}) const [pageNumAndPageSize, setPageNumAndPageSize] = useState({current:1, pageSize:basePageSize})
const [paginationTotal, setPaginationTotal] = useState(0) const [paginationTotal, setPaginationTotal] = useState(0)
const [dictionaryOperationCategory, setDictionaryOperationCategory] = useState(new Map())
const [operations, setOperations] = useState([]) const [operations, setOperations] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState(defaultColumns)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => invokeWebApiWrapperAsync(
async () => { async () => {
const categories = await WellOperationService.getCategories(idWell) let categories = await WellOperationService.getCategories(idWell) ?? []
const dictCategories = new Map() categories = categories.map((item) => ({ value: item.id, label: item.name }))
categories?.forEach((item) => dictCategories.set(item.id, item.name))
setDictionaryOperationCategory(dictCategories) let sectionTypes = await WellOperationService.getSectionTypes(idWell) ?? []
}),[idWell]) sectionTypes = Object.keys(sectionTypes).map((key) => ({ value: parseInt(key), label: sectionTypes[key] }))
setColumns(preColumns => {
preColumns[0] = makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { editable: true, width: 160 })
preColumns[1] = makeSelectColumn('Операция', 'idCategory', categories, undefined, { editable: true, width: 200 })
return preColumns
})
},
setShowLoader,
'Не удалось загрузить список операций по скважине'
), [idWell])
const updateOperations = () => invokeWebApiWrapperAsync( const updateOperations = () => invokeWebApiWrapperAsync(
async () => { async () => {
@ -51,78 +85,39 @@ export const WellOperationsEditor = ({idWell, idType}) => {
useEffect(updateOperations, [idWell, idType, pageNumAndPageSize]) useEffect(updateOperations, [idWell, idType, pageNumAndPageSize])
const columns = [ const handlerProps = {
makeColumn('Конструкция секции','idWellSectionType', { service: WellOperationService,
editable:true, setLoader: setShowLoader,
input:<SelectFromDictionary dictionary={dictionarySectionType}/>, errorMsg: `Не удалось выполнить операцию`,
width:160, onComplete: updateOperations,
render:(_, record)=>getByKeyOrReturnKey(dictionarySectionType, record.idWellSectionType) idWell
}),
makeColumn('Операция','idCategory', {
editable:true,
input:<SelectFromDictionary dictionary={dictionaryOperationCategory}/>,
width:200,
render:(_, record)=>getByKeyOrReturnKey(dictionaryOperationCategory, record.idCategory)
}),
makeColumn('Доп. инфо','categoryInfo', {editable:true, width:300, input:<TextArea/>}),
makeColumn('Глубина забоя на начало','depthStart', makeNumericColumnOptions(2, 'depthStart')),
makeColumn('Глубина забоя при завершении','depthEnd', makeNumericColumnOptions(2, 'depthEnd')),
makeColumn('Время начала','dateStart', {
editable:true,
width:200,
input:<DatePickerWrapper/>,
initialValue:moment().format(),
sorter: makeDateSorter('dateStart'),
render:(_, record) =>
<div className={'text-align-r-container'}>
<span>{moment.utc(record.dateStart).local().format(format)}</span>
</div>
}),
makeColumn('Часы','durationHours', makeNumericColumnOptions(2, 'durationHours')),
makeColumn('Комментарий','comment', {editable:true, input:<TextArea/>}),
]
const onAdd = async (operation) => {
operation.idType = idType
operation.wellDepth = +operation.wellDepth
operation.durationHours = +operation.durationHours
await WellOperationService.insertRange(idWell, [operation])
updateOperations()
} }
const onEdit= async (operation) => { const recordParser = (record) => ({
if(!operation.id) ...record,
return idType,
operation.idType = idType wellDepth: +record.wellDepth,
operation.wellDepth = +operation.wellDepth durationHours: +record.durationHours
operation.durationHours = +operation.durationHours })
await WellOperationService.update(idWell, operation.id, operation)
updateOperations()
}
const onDelete= async (operation) => { return (
if(!operation.id) <LoaderPortal show={showLoader}>
return
await WellOperationService.delete(idWell, operation.id)
updateOperations()
}
return <LoaderPortal show={showLoader}>
<EditableTable <EditableTable
size='small' size={'small'}
bordered bordered
columns={columns} columns={columns}
dataSource={operations} dataSource={operations}
onRowAdd={onAdd} onRowAdd={makeActionHandler('insertRange', handlerProps, recordParser)}
onRowEdit={onEdit} onRowEdit={makeActionHandler('update', handlerProps, recordParser)}
onRowDelete={onDelete} onRowDelete={makeActionHandler('delete', handlerProps)}
pagination={{ pagination={{
current: pageNumAndPageSize.current, current: pageNumAndPageSize.current,
pageSize: pageNumAndPageSize.pageSize, pageSize: pageNumAndPageSize.pageSize,
showSizeChanger: false, showSizeChanger: false,
total: paginationTotal, total: paginationTotal,
onChange: (page, pageSize) => setPageNumAndPageSize({current: page, pageSize: pageSize}) onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
}} }}
/> />
</LoaderPortal> </LoaderPortal>
)
} }

View File

@ -1,8 +1,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import LoaderPortal from '../../components/LoaderPortal' import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory' import { invokeWebApiWrapperAsync } from '../../components/factory'
import { Table, makeColumn, makeColumnsPlanFact } from '../../components/Table' import { Table, makeColumn, makeColumnsPlanFact } from '../../components/Table'
import { OperationStatService } from '../../services/api' import { OperationStatService } from '../../services/api'
import { calcDuration } from '../../utils/datetime'
const makeNumberRender = value => { const makeNumberRender = value => {
const v = +value const v = +value
@ -13,23 +15,15 @@ const makeNumberRender = value => {
const columns = [ const columns = [
makeColumn('Тип секции', 'sectionType'), makeColumn('Тип секции', 'sectionType'),
makeColumnsPlanFact('Глубина' ,'wellDepth', {render:makeNumberRender}), makeColumnsPlanFact('Глубина, м' ,'wellDepth', { render: makeNumberRender }),
makeColumnsPlanFact('Часы' ,'duration', { render: makeNumberRender }), makeColumnsPlanFact('Часы' ,'duration', { render: makeNumberRender }),
makeColumnsPlanFact('МСП' ,'rop', {render:makeNumberRender}), makeColumnsPlanFact('МСП, м/ч' ,'rop', { render: makeNumberRender }),
makeColumnsPlanFact('Рейсовая скорость' ,'routeSpeed', {render:makeNumberRender}), makeColumnsPlanFact('Рейсовая скорость, м/ч','routeSpeed', { render: makeNumberRender }),
makeColumnsPlanFact('Подъем КНБК' ,'bhaUpSpeed', {render:makeNumberRender}), makeColumnsPlanFact('Подъем КНБК, м/ч' ,'bhaUpSpeed', { render: makeNumberRender }),
makeColumnsPlanFact('Спуск КНБК' ,'bhaDownSpeed', {render:makeNumberRender}), makeColumnsPlanFact('Спуск КНБК, м/ч' ,'bhaDownSpeed', { render: makeNumberRender }),
makeColumnsPlanFact('Спуск ОК' ,'casingDownSpeed', {render:makeNumberRender}), makeColumnsPlanFact('Спуск ОК, м/ч' ,'casingDownSpeed', { render: makeNumberRender }),
] ]
const calcDuration = (start, end) => {
const msInDay = 1000 * 60 * 60 * 24
const startD = new Date(start)
const endD = new Date(end)
const ms = endD - startD
return ms / msInDay
}
export const WellSectionsStat = ({idWell}) => { export const WellSectionsStat = ({idWell}) => {
const [sections, setSections] = useState([]) const [sections, setSections] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)

View File

@ -34,3 +34,34 @@ input.measure-input {
.ml-10px { .ml-10px {
margin-left: 10px; margin-left: 10px;
} }
.v-div {
display: flex;
height: 180px;
justify-content: center;
}
.v-span {
white-space: pre;
vertical-align: center;
text-align: center;
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
}
.measure-column-header {
border: 1px solid lightgrey;
}
.measure-column-value {
border: 1px solid lightgrey;
justify-content: right;
margin-right: 16px;
font-weight: bold;
text-align: right;
padding: 0;
}

View File

@ -1,5 +0,0 @@
export type RawDate = number | string | Date
export function isRawDate(value: unknown): value is RawDate {
return isNaN(new Date(value as RawDate).getTime())
}

View File

@ -1,10 +1,44 @@
export const periodToString = (time?: number) => { import moment from 'moment'
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') export type RawDate = number | string | Date
return `${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}` export const defaultFormat: string = 'YYYY.MM.DD HH:mm'
export enum timeInS {
millisecond = 0.001,
second = 1,
minute = second * 60,
hour = minute * 60,
day = hour * 24,
week = day * 7,
}
export function isRawDate(value: unknown): value is RawDate {
return !isNaN(Date.parse(String(value)))
}
export const formatDate = (date: unknown, utc = false, format = defaultFormat) => {
if (!isRawDate(date)) return null
const out = utc ? moment.utc(date).local() : moment(date)
return out.format(format)
}
export const periodToString = (time?: number) => {
if (!time || time <= 0) return '00:00:00'
const days = Math.floor(time / timeInS.day)
time %= timeInS.day
const hours = Math.floor(time / timeInS.hour)
time %= timeInS.hour
const minutes = Math.floor(time / timeInS.minute)
time %= timeInS.minute
const seconds = Math.floor(time / timeInS.second)
const toFixed = (num: number) => String(num).padStart(2, '0')
return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}`
}
export const calcDuration = (start: RawDate, end: RawDate) => {
if (!isRawDate(start) || !isRawDate(end)) return
return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day
} }

View File

@ -1,5 +1,5 @@
export type { RawDate } from './DateTimeUtils' export type { RawDate, timeInS } from './datetime'
export { isRawDate } from './DateTimeUtils' export { isRawDate, formatDate, defaultFormat, periodToString } from './datetime'
export const headerHeight = 64 export const headerHeight = 64