Merge remote-tracking branch 'origin/dev' into DrillingModesPage

This commit is contained in:
goodmice 2021-10-12 11:13:55 +05:00
commit f9271b0f46
35 changed files with 757 additions and 285 deletions

View File

@ -32,7 +32,7 @@
"react_test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://192.168.1.70:5000",
"proxy": "http://192.168.1.58:5000",
"eslintConfig": {
"extends": [
"react-app",

View File

@ -0,0 +1,7 @@
export class ErrorFetch extends Error {
constructor(status, message) {
super(message);
this.name = "ErrorFetch"
this.status = status
}
}

View File

@ -1,4 +1,6 @@
import { ReactNode } from 'react'
import { makeNumericSorter, makeStringSorter} from './sorters'
export { makeDateSorter, makeNumericSorter, makeStringSorter} from './sorters'
export { Table } from 'antd'
export { EditableTable } from './EditableTable'
export { DatePickerWrapper } from './DatePickerWrapper'
@ -7,10 +9,28 @@ export { SelectFromDictionary } from './SelectFromDictionary'
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
export const formatDate='YYYY.MM.DD HH:mm'
export const numericColumnOptions = {
export const makeNumericRender = (fixed?:number) => (value: any, row: object): ReactNode => {
const placeholder = '-'
let val = placeholder
if((value !== null) ||
(value !== undefined) ||
Number.isNaN(value) ||
!Number.isFinite(value)){
val = !!fixed
? (+value).toFixed(fixed)
: (+value).toPrecision(5)
}
return (<div className='text-align-r-container'>
<span>{val}</span>
</div>)
}
export const makeNumericColumnOptions = (fixed?:number, sorterKey?:string ) => ({
editable: true,
initialValue: 0,
width:100,
sorter: sorterKey? makeNumericSorter(sorterKey) : null,
formItemRules: [
{
required: true,
@ -18,7 +38,8 @@ export const numericColumnOptions = {
pattern: RegExpIsFloat,
},
],
};
render: makeNumericRender(fixed),
})
/*
other - объект с дополнительными свойствами колонки
@ -78,41 +99,6 @@ export const makeColumnsPlanFact = (title:string | ReactNode, key:string|string[
export const makeFilterTextMatch = (key: string | number) => (filterValue: string | number, dataItem: any) =>
dataItem[key] === filterValue
export const makeNumericSorter = (key: string) => (a: any, b: any) => a[key] - b[key]
export const makeStringSorter = (key: string) => (a: any, b: any) =>
{
if(a == null && b == null)
return 1
if(a == null)
return 1
if(b == null)
return -1
let aValue = a[key]
let bValue = b[key]
for (let i = 0; i < a.length; i++) {
if (isNaN(aValue.charCodeAt(i)) || (aValue.charCodeAt(i) > bValue.charCodeAt(i)))
return 1
if (aValue.charCodeAt(i) > bValue.charCodeAt(i))
return -1
}
return 0
}
export const makeDateSorter = (key: string) => (a: any, b: any) => {
const date = new Date(a[key])
if(Number.isNaN(date.getTime()))
throw new Error('Date column contains not date formatted string(s)')
return date.getTime() - new Date(b[key]).getTime()
}
export const makeGroupColumn = (title: string, children: object[]) => ({
title: title,
children: children,
@ -135,17 +121,6 @@ export const makeTextColumn = (
...other
})
export const defaultNumericRender = (value: any, row: object) => {
const placeholder = '-'
if((value === null) ||
(value === undefined) ||
Number.isNaN(value) ||
!Number.isFinite(value))
return placeholder
return (+value).toPrecision(5)
}
export const makeNumericColumn = (title: string, dataIndex: string,
filters: object[], filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any, width: string, other?: columnPropsOther) => ({
@ -156,7 +131,7 @@ export const makeNumericColumn = (title: string, dataIndex: string,
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
sorter: makeNumericSorter(dataIndex),
width: width,
render: renderDelegate??defaultNumericRender,
render: renderDelegate??makeNumericRender(),
align: 'right',
...other
})

View File

@ -0,0 +1,33 @@
export const makeNumericSorter = (key: string) => (a: any, b: any) => a[key] - b[key];
export const makeStringSorter = (key: string) => (a: any, b: any) => {
if (a == null && b == null)
return 1;
if (a == null)
return 1;
if (b == null)
return -1;
let aValue = a[key];
let bValue = b[key];
for (let i = 0; i < a.length; i++) {
if (isNaN(aValue.charCodeAt(i)) || (aValue.charCodeAt(i) > bValue.charCodeAt(i)))
return 1;
if (aValue.charCodeAt(i) > bValue.charCodeAt(i))
return -1;
}
return 0;
};
export const makeDateSorter = (key: string) => (a: any, b: any) => {
const date = new Date(a[key]);
if (Number.isNaN(date.getTime()))
throw new Error('Date column contains not date formatted string(s)');
return date.getTime() - new Date(b[key]).getTime();
};

View File

@ -2,22 +2,38 @@ import { Upload, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { useState } from 'react'
import { upload } from './factory'
import { ErrorFetch } from './ErrorFetch'
export default function UploadForm({url, accept, onUploadStart, onUploadComplete, onUploadError}) {
export const UploadForm = ({url, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError}) => {
const [fileList, setfileList] = useState([])
const handleFileSend = async (values) => {
const handleFileSend = async () => {
if(onUploadStart)
onUploadStart()
try {
const formData = new FormData()
const formDataLocal = new FormData()
fileList.forEach((val) => {
formData.append("files", val.originFileObj);
});
await upload(url, formData)
formDataLocal.append("files", val.originFileObj)
})
if(formData)
for(var propName in formData)
formDataLocal.append(propName, formData[propName])
const response = await upload(url, formDataLocal)
if(!response.ok)
{
const errorText = await response.text()
const error = new ErrorFetch(response.status, errorText)
throw error
}
else{
if(onUploadSuccess)
onUploadSuccess()
}
} catch(error) {
if(onUploadError)
onUploadError(error)
onUploadError(error)
} finally {
setfileList([])
if(onUploadComplete)
@ -27,7 +43,7 @@ export default function UploadForm({url, accept, onUploadStart, onUploadComplete
const isSendButtonEnabled = fileList.length > 0
return(
<div style={{display: 'flex'}}>
<div style={{display: 'flex', ...style}}>
<Upload
name ="file"
fileList={fileList}

View File

@ -18,7 +18,7 @@ export const UserView = ({user}) => {
<GridItem row={2} col={1}>
Фамилия:
</GridItem>
<GridItem row={3} col={2}>
<GridItem row={2} col={2}>
{user?.surname}
</GridItem>
<GridItem row={3} col={1}>

View File

@ -81,13 +81,14 @@ export const download = async (url:string, fileName?:string) => {
}
export const upload = async (url:string, formData: FormData) => {
await fetch(url, {
let response = await fetch(url, {
headers: {
Authorization: 'Bearer ' + localStorage['token']
},
method: 'Post',
body: formData,
})
return response
}
export const downloadFile = async (fileInfo: FileInfoDto) => {

View File

@ -5,7 +5,7 @@ import {
Row,
Col,
Tooltip} from 'antd'
import { DataService } from '../services/api'
import { TelemetryDataSaubService } from '../services/api'
import {generateUUID} from '../services/UidGenerator'
import { ArchiveColumn } from '../components/ArchiveColumn'
import moment from 'moment'
@ -94,7 +94,7 @@ export default function Archive({idWell}) {
let startDate = rangeDate[0].toISOString()
setLoader(true)
DataService.getData(idWell, startDate, interval, 2048)
TelemetryDataSaubService.getData(idWell, startDate, interval, 2048)
.then(handleReceiveDataSaub)
.catch(error => {
notify(`Не удалось загрузить данные по скважине (${idWell}) c ${rangeDate[0]} по ${rangeDate[1]}`, 'error')

View File

@ -7,7 +7,7 @@ import {
makeNumericColumnPlanFact
} from "../../components/Table";
import { invokeWebApiWrapperAsync } from '../../components/factory';
import ChartDepthToDay from '../../components/charts/ChartDepthToDay';
import ChartTvD from '../WellOperations/ChartTvD';
import WellOperationsTable from './WellOperationsTable'
import {
calcAndUpdateStatsBySections,
@ -249,10 +249,10 @@ export default function ClusterSections({ clusterData }) {
width={1500}
footer={null}
>
<ChartDepthToDay
<ChartTvD
dataPlan={tvdDataPlan}
dataFact={tvdDataFact}
dataForecast={tvdDataForecast} />
dataPredict={tvdDataForecast} />
</Modal>
<Modal

View File

@ -6,12 +6,11 @@ import {
makeTextColumn,
makeGroupColumn,
makeColumn,
makeNumericColumnPlanFact,
makeDateSorter
} from "../../components/Table";
makeDateSorter,
makeNumericColumnPlanFact} from "../../components/Table";
import { calcAndUpdateStatsBySections, makeFilterMinMaxFunction } from "./functions";
import { invokeWebApiWrapperAsync } from '../../components/factory';
import ChartDepthToDay from '../../components/charts/ChartDepthToDay';
import ChartTvD from '../WellOperations/ChartTvD';
import WellOperationsTable from './WellOperationsTable'
import { getOperations } from "./functions";
@ -206,10 +205,10 @@ useEffect(() => {
width={1500}
footer={null}
>
<ChartDepthToDay
<ChartTvD
dataPlan={tvdDataPlan}
dataFact={tvdDataFact}
dataForecast={tvdDataForecast}
dataPredict={tvdDataForecast}
/>
</Modal>

View File

@ -6,17 +6,24 @@ const minPrefix = "isMin"
export const getOperations = async (idWell) => {
const ops = await WellOperationStatService.getTvd(idWell);
const planData = ops.map(el => {
return {key: el.plan?.id, depth: el.plan?.wellDepth, date: el.plan?.startDate}
}).filter(el => el.key)
const convert = wellOperationDto =>
({
key: wellOperationDto?.id,
depth: wellOperationDto?.depthStart,
date: wellOperationDto?.dateStart
})
const factData = ops.map(el => {
return {key: el.fact?.id, depth: el.fact?.wellDepth, date: el.fact?.startDate}
}).filter(el => el.key)
const planData = ops
.map(item => convert(item.plan))
.filter(el => el.key)
const predictData = ops.map(el => {
return {key: el.predict?.id, depth: el.predict?.wellDepth, date: el.predict?.startDate}
}).filter(el => el.key)
const factData = ops
.map(item => convert(item.fact))
.filter(el => el.key)
const predictData = ops
.map(item => convert(item.predict))
.filter(el => el.key)
return { operations: ops, plan: planData, fact: factData, predict: predictData }
}

View File

@ -8,7 +8,7 @@ import {
formatBytes,
} from "../../components/factory"
import { EditableTable, makePaginationObject } from "../../components/Table"
import UploadForm from "../../components/UploadForm"
import {UploadForm} from "../../components/UploadForm"
import LoaderPortal from "../../components/LoaderPortal"
import {UserView} from '../../components/UserView'
import {CompanyView} from '../../components/CompanyView'

View File

@ -1,54 +1,198 @@
import { useState, useEffect } from 'react'
import { Table, Button, Modal } from 'antd'
import { HourglassOutlined } from '@ant-design/icons'
import { Button, Form, Input, Popconfirm, Timeline } from 'antd'
import moment from 'moment'
import { CheckSquareOutlined,
EditOutlined,
SaveOutlined,
PlusOutlined,
CloseCircleOutlined,
DeleteOutlined } from '@ant-design/icons'
import { View } from './View'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { MeasureService } from '../../services/api'
import { Editor } from './Editor'
import '../../styles/index.css'
import '../../styles/measure.css'
export const MeasureTable = ({idWell, idCategory, title, columns}) => {
const [showLoader, setShowLoader] = useState(false)
const [showEditor, setShowEditor] = useState(false)
const [lastData, setLastData] = useState({})
const format='YYYY.MM.DD HH:mm'
const update = ()=>invokeWebApiWrapperAsync(async()=>{
const data = await MeasureService.getLast(idWell, idCategory)
setLastData(data)
export const MeasureTable = ({idWell, idCategory, title, columns, values, updateMeasuresFunc}) => {
const [showLoader, setShowLoader] = useState(false);
const [displayedValues, setDisplayedValues] = useState({});
const [editingColumns, setEditingColumns] = useState(columns);
const [isTableEditing, setIsTableEditing] = useState(false);
const [editingActionName, setEditingActionName] = useState('');
const [measuresForm] = Form.useForm();
const createEditingColumns = (cols, renderDelegate) =>
cols.map(col =>
({ render: renderDelegate,
...col
})
)
useEffect(() => {
const defaultValuesToDisplay = values[values.length-1]
setDisplayedValues(defaultValuesToDisplay)
}, [values])
useEffect(() => {
let switchableColumns = []
isTableEditing
? switchableColumns = createEditingColumns(columns, () => <Input className='w-100 measure-input' />)
: switchableColumns = createEditingColumns(columns, null)
if(editingActionName === 'edit')
measuresForm.setFieldsValue(displayedValues?.data);
else if(editingActionName === 'add')
measuresForm.resetFields()
setEditingColumns(switchableColumns)
}, [isTableEditing, columns, editingActionName, displayedValues?.data, measuresForm])
const markMeasuresAsDeleted = async () => {
setShowLoader(true)
await MeasureService.markAsDelete(idWell, displayedValues.id)
updateMeasuresFunc()
setShowLoader(false)
}
, setShowLoader
, "не удалось загрузить")
useEffect(update, [idWell, idCategory])
const checkIsDataDefault = () =>
displayedValues?.isDefaultData ? true : false
const timestamp = lastData ? new Date(lastData?.timestamp).toLocaleString() : '-'
return <LoaderPortal show={showLoader}>
<h3>{title}</h3>
<span>Дата: {timestamp}</span>
&nbsp;
<Button
onClick={() => setShowEditor(true)}
icon={<HourglassOutlined/>}>История</Button>
<Table
style={{marginTop:16}}
bordered
dataSource = {[lastData?.data]}
columns = {columns}
scroll={{ x: 400, y: 600 }}/>
<Modal
title={title}
centered
visible={showEditor}
onOk={() => setShowEditor(false)}
onCancel={() => setShowEditor(false)}
width="95%"
footer={null}
const crudButtons =
<div className='w-300px mt-8px'>
<Button
key='add'
className='w-33'
onClick={() => {
setEditingActionName('add')
setIsTableEditing(true)
}}
>
<PlusOutlined />
</Button>
<Button
key='edit'
className='w-33'
onClick={() => {
setEditingActionName('edit')
setIsTableEditing(true)
}}
disabled={checkIsDataDefault()}
>
<EditOutlined />
</Button>
<Popconfirm
title="Удалить данные?"
onConfirm={() => markMeasuresAsDeleted()}
disabled={checkIsDataDefault()}
>
<Button
key='delete'
onClick={() => {
setEditingActionName('delete')
}}
disabled={checkIsDataDefault()}
>
<Editor
idWell={idWell}
idCategory={idCategory}
columns = {columns}
onUpdate={update}/>
</Modal>
</LoaderPortal>
<DeleteOutlined style={{margin:'auto 28px'}}/>
</Button>
</Popconfirm>
</div>
const confirmButtons =
<div className='w-300px mt-8px'>
<div className='d-flex'>
<Button
key='confirm'
className='w-50'
onClick={() => { measuresForm.submit() }}
>
<SaveOutlined />
</Button>
<Button
key='decline'
className='w-50'
onClick={()=> setIsTableEditing(false)}
>
<CloseCircleOutlined />
</Button>
</div>
</div>
let handleSubmitMeasuresForm = async (formData) => {
measuresForm.validateFields()
const measureParams = {
idWell: idWell,
idCategory: idCategory,
timestamp: new Date().toISOString(),
data: formData
}
setShowLoader(true)
if(editingActionName === 'add') {
await MeasureService.insert(idWell, measureParams)
} else if (editingActionName === 'edit') {
measureParams.id = displayedValues.id
measureParams.timestamp = displayedValues.timestamp
await MeasureService.update(idWell, measureParams)
}
setIsTableEditing(false)
updateMeasuresFunc()
setShowLoader(false)
}
return <>
&nbsp;
<h2>{title}</h2>
&nbsp;
<div className='d-flex'>
<div className='flex-direction-column'>
<div className='measure-buttons-container'>
{isTableEditing
? confirmButtons
: crudButtons
}
</div>
<div className='measure-dates mt-20px'>
<Timeline className='mt-12px ml-10px'>
{values.map((item, index) =>
<Timeline.Item
key={index}
className='measure-button'
onClick={() => setDisplayedValues(item)}
dot={item?.id === displayedValues?.id
? <CheckSquareOutlined className="timeline-clock-icon" />
: null}
>
<span className={item?.id === displayedValues?.id ? 'selected-timeline' : ''}>
{item.timestamp ? moment.utc(item.timestamp).local().format(format) : 'Нет данных'}
</span>
</Timeline.Item>
)}
</Timeline>
</div>
</div>
<div className='w-100'>
<LoaderPortal show={showLoader}>
<Form
form={measuresForm}
onFinish={handleSubmitMeasuresForm}
>
<View
item={displayedValues?.data ?? {}}
columns={editingColumns}
/>
</Form>
</LoaderPortal>
</div>
</div>
</>
}

View File

@ -1,48 +1,40 @@
import { useState, useEffect } from 'react'
import { Button, Modal, Timeline } from 'antd'
import moment from 'moment'
import { Table, Button, Modal } from 'antd'
import { HourglassOutlined } from '@ant-design/icons'
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { MeasureService } from '../../services/api'
import { Editor } from './Editor'
import TimelineItem from 'antd/lib/timeline/TimelineItem'
//import { View } from './View'
const format='YYYY.MM.DD HH:mm'
export const MeasureTable2 = ({idWell, idCategory, title, columns}) => {
export const MeasureTable = ({idWell, idCategory, title, columns}) => {
const [showLoader, setShowLoader] = useState(false)
const [showEditor, setShowEditor] = useState(false)
const [history, setHistory] = useState([])
const [lastData, setLastData] = useState({})
const update = () => invokeWebApiWrapperAsync(async()=>{
const data = await MeasureService.getHisory(idWell, idCategory)
const story = data?.map( i=> ({
id: i.id,
idWell: i.idWell,
idCategory: i.idCategory,
timestamp: moment.utc(i.timestamp).local().format(format),
...i.data}))
setHistory(story??[])
const update = ()=>invokeWebApiWrapperAsync(async()=>{
const data = await MeasureService.getLast(idWell, idCategory)
setLastData(data)
}
, setShowLoader
, "не удалось загрузить")
useEffect(update, [idWell, idCategory])
const timestamp = lastData ? new Date(lastData?.timestamp).toLocaleString() : '-'
return <LoaderPortal show={showLoader}>
<h3>{title}</h3>
<span>Дата: {timestamp}</span>
&nbsp;
<Button
onClick={() => setShowEditor(true)}
icon={<HourglassOutlined/>}>История</Button>
<Timeline>
{history.map(item=><TimelineItem value={item}>{item.timestamp}</TimelineItem>)}
</Timeline>
{/* <View
item = {lastData?.data}
columns = {columns}/> */}
icon={<HourglassOutlined/>}>История</Button>
<Table
style={{marginTop:16}}
bordered
dataSource = {[lastData?.data]}
columns = {columns}
scroll={{ x: 400, y: 600 }}/>
<Modal
title={title}
centered

View File

@ -1,38 +1,62 @@
import { Empty } from 'antd';
import { Empty, Form } from 'antd';
import {Grid, GridItem} from '../../components/Grid'
import '../../styles/index.css'
const renderSwitchableColumn = (column, itm) => {
if(column.render) {
return (
<Form.Item
key={column.dataIndex}
name={column.dataIndex}
style={{margin: 0}}
//rules={column.formItemRules}
>
{column.render(itm[column.dataIndex])}
</Form.Item>
)
}
return <p key={column.title} className='m-5px'>{itm[column.dataIndex]}</p>
}
export const View = ({columns, item}) => {
if (!item || !columns?.length)
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
return <Empty key='empty' image={Empty.PRESENTED_IMAGE_SIMPLE}/>
const colsCount = 3
const viewItems = columns.map( (column, i) => {
const row = Math.floor(i / colsCount) + 1
const colb = i % colsCount
return <>
<GridItem
key={column.dataIndex}
row={row}
col={colb*2 + 1}
background='#00000005'
border='1px solid #FFFE'
>
style={{border:'1px solid lightgrey'}}
>
{column.title}
</GridItem>
<GridItem
key={column.title}
row={row}
col={colb*2 + 2}
border='1px solid rgba(0, 0, 0, 0.05)'
justifyContect='right'
marginRight='16px'
fontWeight='bold'
textAlign='right'>
{column.render ? column.render(item[column.dataIndex]) : item[column.dataIndex]}
style={{border:'1px solid lightgrey',
justifyContent:'right',
marginRight:'16px',
fontWeight:'bold',
textAlign:'right',
padding: 0}}
>
{renderSwitchableColumn(column, item)}
</GridItem>
</>
})
return <Grid>
{viewItems}
</Grid>
return <>
<Grid>
{viewItems}
</Grid>
</>
}

View File

@ -42,4 +42,10 @@ export const numericColumnOptions = {
export const textColumnOptions = {
editable:true,
input:<TextArea/>,
width:'20rem'}
width:'20rem',
formItemRules: [
{
required: true,
message: `Введите текст`
},
],}

View File

@ -1,39 +0,0 @@
import {makeColumn} from '../../components/Table'
import {v, numericColumnOptions, textColumnOptions} from './columnsCommon'
export const columnsMudDiagram = [
makeColumn(v('N пробы'), 'probeNumber', numericColumnOptions),
makeColumn(v('Глубина отбора пробы'), 'probeExtractionDepth', numericColumnOptions),
{
title: 'Литология',
key: 'lithology',
children: [
makeColumn(v('Песчаник (%)'), 'sandstone', numericColumnOptions),
makeColumn(v('Алевролит (%)'), 'siltstone', numericColumnOptions),
makeColumn(v('Аргиллит (%)'), 'argillit', numericColumnOptions),
makeColumn(v('Аргиллит бит. (%)'), 'brokenArgillit', numericColumnOptions),
makeColumn(v('Уголь (%)'), 'coal', numericColumnOptions),
makeColumn(v('Песок (%)'), 'sand', numericColumnOptions),
makeColumn(v('Глина (%)'), 'clay', numericColumnOptions),
makeColumn(v('Известняк (%)'), 'camstone', numericColumnOptions),
makeColumn(v('Цемент (%)'), 'cement', numericColumnOptions),
]
},
makeColumn('Краткое описание', 'summary', textColumnOptions),
makeColumn(v('ЛБА бурового раствора'), 'drillingMud', numericColumnOptions),
makeColumn(v('ЛБА (шлама)'), 'sludge', numericColumnOptions),
{
title: 'Газопоказания',
key: 'gasIndications',
children: [
makeColumn(v('Сумма УВ мах. (абс%)'), 'maxSum', numericColumnOptions),
makeColumn(v('С1 метан (отн%)'), 'methane', numericColumnOptions),
makeColumn(v('С2 этан (отн%)'), 'ethane', numericColumnOptions),
makeColumn(v('С3 пропан (отн%)'), 'propane', numericColumnOptions),
makeColumn(v('С4 бутан (отн%)'), 'butane', numericColumnOptions),
makeColumn(v('С5 пентан (отн%)'), 'pentane', numericColumnOptions),
]
},
makeColumn(v('Мех. скорость'), 'mechanicalSpeed', numericColumnOptions),
makeColumn('Предварительное заключение о насыщении по ГК', 'preliminaryConclusion', textColumnOptions),
]

View File

@ -31,3 +31,40 @@ export const columnsDrillingFluid = [
makeColumn(v("Смазка, %"), "grease",numericColumnOptions),
makeColumn(v("Карбонат кальция, кг/м³"), "calciumCarbonate",numericColumnOptions),
];
export const drillingFluidDefaultData = {
idWell: 0,
key: 'drillingFluidDefaultData',
idCategory: 0,
isDefaultData: true,
data: {
"name": 0,
"temperature": 0,
"density": 0,
"conditionalViscosity": 0,
"r300": 0,
"r600": 0,
"r3r6": 0,
"dnsDpa": 0,
"plasticViscocity": 0,
"snsDpa": 0,
"r3r649С": 0,
"dns49Cdpa": 0,
"plasticViscocity49c": 0,
"sns49Cdpa": 0,
"mbt": 0,
"sand": 0,
"filtering": 0,
"crust": 0,
"ktk": 0,
"ph": 0,
"hardness": 0,
"chlorides": 0,
"pf": 0,
"mf": 0,
"pm": 0,
"fluidSolidPhase": 0,
"grease": 0,
"calciumCarbonate": 0
}
}

View File

@ -1,25 +1,64 @@
import { columnsMudDiagram} from './columnsMudDiagram'
import { columnsDrillingFluid} from './columnsDrillingFluid'
import { columnsNnb } from './columnsNnb'
import { useState, useEffect } from 'react'
import { columnsMudDiagram} from './mudDiagramData'
import { mudDiagramDefaultData} from './mudDiagramData'
import { columnsDrillingFluid} from './drillingFluidData'
import { drillingFluidDefaultData} from './drillingFluidData'
import { columnsNnb } from './nnbData'
import { nnbDefaultData } from './nnbData'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { MeasureService } from '../../services/api'
import LoaderPortal from '../../components/LoaderPortal'
import { MeasureTable } from './MeasureTable'
//import { MeasureTable2 } from './MeasureTable2'
export default function Measure({idWell}){
const [showLoader, setShowLoader] = useState(false)
const [fluidValues, setFluidValues] = useState([])
const [mudValues, setMudValues] = useState([])
const [nnbValues, setNnbValues] = useState([])
const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(false)
const updateCurrentValues = () => invokeWebApiWrapperAsync(async()=>{
const measures = await MeasureService.getHisory(idWell)
setIsMeasuresUpdating(false)
const fluids = measures.filter(el => el.idCategory === 1)
setFluidValues(fluids.length ? fluids : [drillingFluidDefaultData])
const muds = measures.filter(el => el.idCategory === 2)
setMudValues(muds.length ? muds : [mudDiagramDefaultData])
const nnbs = measures.filter(el => el.idCategory === 3)
setNnbValues(nnbs.length ? nnbs : [nnbDefaultData])
}
,setShowLoader
,`Не удалось загрузить последние данные по скважине ${idWell}`)
useEffect(updateCurrentValues, [idWell, isMeasuresUpdating])
return <>
<MeasureTable
idWell={idWell}
idCategory={1}
title='Замер бурового раствора'
columns={columnsDrillingFluid}/>
<MeasureTable
idWell={idWell}
idCategory={2}
title='Шламограмма'
columns={columnsMudDiagram}/>
<MeasureTable
idWell={idWell}
idCategory={3}
title='ННБ'
columns={columnsNnb}/>
<LoaderPortal show={showLoader}>
<MeasureTable
idWell={idWell}
idCategory={1}
title='Замер бурового раствора'
columns={columnsDrillingFluid}
values={fluidValues}
updateMeasuresFunc = {() => setIsMeasuresUpdating(true)}
/>
<MeasureTable
idWell={idWell}
idCategory={2}
title='Шламограмма'
columns={columnsMudDiagram}
values={mudValues}
updateMeasuresFunc = {() => setIsMeasuresUpdating(true)}
/>
<MeasureTable
idWell={idWell}
idCategory={3}
title='ННБ'
columns={columnsNnb}
values={nnbValues}
updateMeasuresFunc = {() => setIsMeasuresUpdating(true)}
/>
</LoaderPortal>
</>
}

View File

@ -0,0 +1,58 @@
import {makeColumn} from '../../components/Table'
import {v, numericColumnOptions, textColumnOptions} from './columnsCommon'
export const columnsMudDiagram = [
makeColumn(v('N пробы'), 'probeNumber', numericColumnOptions),
makeColumn(v('Глубина отбора пробы'), 'probeExtractionDepth', numericColumnOptions),
makeColumn(v('Песчаник (%)'), 'sandstone', numericColumnOptions),
makeColumn(v('Алевролит (%)'), 'siltstone', numericColumnOptions),
makeColumn(v('Аргиллит (%)'), 'argillit', numericColumnOptions),
makeColumn(v('Аргиллит бит. (%)'), 'brokenArgillit', numericColumnOptions),
makeColumn(v('Уголь (%)'), 'coal', numericColumnOptions),
makeColumn(v('Песок (%)'), 'sand', numericColumnOptions),
makeColumn(v('Глина (%)'), 'clay', numericColumnOptions),
makeColumn(v('Известняк (%)'), 'camstone', numericColumnOptions),
makeColumn(v('Цемент (%)'), 'cement', numericColumnOptions),
makeColumn('Краткое описание', 'summary', textColumnOptions),
makeColumn(v('ЛБА бурового раствора'), 'drillingMud', numericColumnOptions),
makeColumn(v('ЛБА (шлама)'), 'sludge', numericColumnOptions),
makeColumn(v('Сумма УВ мах. (абс%)'), 'maxSum', numericColumnOptions),
makeColumn(v('С1 метан (отн%)'), 'methane', numericColumnOptions),
makeColumn(v('С2 этан (отн%)'), 'ethane', numericColumnOptions),
makeColumn(v('С3 пропан (отн%)'), 'propane', numericColumnOptions),
makeColumn(v('С4 бутан (отн%)'), 'butane', numericColumnOptions),
makeColumn(v('С5 пентан (отн%)'), 'pentane', numericColumnOptions),
makeColumn(v('Мех. скорость'), 'mechanicalSpeed', numericColumnOptions),
makeColumn('Предварительное заключение о насыщении по ГК', 'preliminaryConclusion', textColumnOptions),
]
export const mudDiagramDefaultData = {
idWell: 0,
key: 'mudDiagramDefaultData',
idCategory: 0,
isDefaultData: true,
data: {
"probeNumber": 0,
"probeExtractionDepth": 0,
"sandstone": 0,
"siltstone": 0,
"argillit": 0,
"brokenArgillit": 0,
"coal": 0,
"sand": 0,
"clay": 0,
"camstone": 0,
"cement": 0,
"summary": '-',
"drillingMud": 0,
"sludge": 0,
"maxSum": 0,
"methane": 0,
"ethane": 0,
"propane": 0,
"butane": 0,
"pentane": 0,
"mechanicalSpeed": 0,
"preliminaryConclusion": '-'
}
}

View File

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

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from "react"
import { Table, makeDateSorter, makeNumericSorter, formatDate} from "../../components/Table"
import { Table, formatDate, makeDateSorter, makeNumericSorter} from "../../components/Table"
import { Button, Tooltip } from "antd"
import { FilePdfOutlined, FileTextOutlined} from '@ant-design/icons'
import { ReportService } from "../../services/api"

View File

@ -46,7 +46,7 @@ export default function Well() {
<Link to={`${rootPath}/report`}>Рапорт</Link>
</PrivateMenuItem>
<PrivateMenuItem key="operations" icon={<FolderOutlined />}>
<Link to={`${rootPath}/operations/plan`}>Операции по скважине</Link>
<Link to={`${rootPath}/operations`}>Операции по скважине</Link>
</PrivateMenuItem>
<PrivateMenuItem key="archive" icon={<DatabaseOutlined />}>
<Link to={`${rootPath}/archive`}>Архив</Link>
@ -89,7 +89,7 @@ export default function Well() {
<Route path="/well/:idWell/report">
<Report idWell={idWell} />
</Route>
<Route path="/well/:idWell/operations/:tab">
<Route path="/well/:idWell/operations">
<WellOperations idWell={idWell} />
</Route>
<Route path="/well/:idWell/archive">

View File

@ -97,7 +97,7 @@ const makeDataset = (data, label, color, width=1.5, dash) => ({
borderDash: dash,
})
export default function ChartDepthToDay({dataPlan, dataFact, dataForecast}) {
export default function ChartTvD({dataPlan, dataFact, dataPredict}) {
const chartRef = useRef(null)
const [chart, setChart] = useState()
@ -105,7 +105,7 @@ export default function ChartDepthToDay({dataPlan, dataFact, dataForecast}) {
let data = {
datasets: [
makeDataset(dataFact, 'Факт', '#0A0'),
makeDataset(dataForecast, 'Прогноз', 'purple', 1, [7,3]),
makeDataset(dataPredict, 'Прогноз', 'purple', 1, [7,3]),
makeDataset(dataPlan, 'План', '#C004', 4),
]
}
@ -127,7 +127,7 @@ export default function ChartDepthToDay({dataPlan, dataFact, dataForecast}) {
chart.data = data
chart.update()
}
}, [chart, dataPlan, dataFact, dataForecast])
}, [chart, dataPlan, dataFact, dataPredict])
return (<canvas ref={chartRef} />)
}

View File

@ -0,0 +1,54 @@
import { Button, Tooltip, Modal } from "antd";
import {useState} from 'react'
import { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons'
import { download } from '../../components/factory'
import { ImportOperations } from './ImportOperations'
const style = {margin:4}
export const ImportExportBar = ({idWell, onImported}) =>{
const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const downloadTemplate = async () => download(`/api/well/${idWell}/wellOperations/tamplate`)
const downloadExport = async () => download(`/api/well/${idWell}/wellOperations/export`)
const onDone = () => {
setIsImportModalVisible(false)
if(onImported)
onImported()
}
return <>
<Tooltip title = "Импорт - загрузить файл с операциями на сервер">
<Button
icon={<ImportOutlined/>}
style={style}
onClick={_=>setIsImportModalVisible(true)}/>
</Tooltip>
<Tooltip title = "Экспорт - скачать файл с операциями по скважине">
<Button
icon={<ExportOutlined/>}
style={style}
onClick={downloadExport}/>
</Tooltip>
<Tooltip title = "Скачать шаблон">
<Button
icon={<FileOutlined/>}
style={style}
onClick={downloadTemplate}/>
</Tooltip>
<Modal
title='Импорт'
visible={isImportModalVisible}
onCancel={_=>setIsImportModalVisible(false)}
footer={null}>
<ImportOperations
idWell={idWell}
onDone={onDone}/>
</Modal >
</>
}

View File

@ -0,0 +1,41 @@
import {Switch} from 'antd'
import {useState} from 'react'
import { ErrorFetch } from '../../components/ErrorFetch'
import {UploadForm} from '../../components/UploadForm'
export const ImportOperations = ({idWell, onDone}) =>{
const [deleteBeforeImport, setDeleteBeforeImport] = useState(false)
const [errorText, setErrorText] = useState('')
const url = `/api/well/${idWell}/wellOperations/import`
const onUploadSuccess = () => {
setErrorText('')
if(onDone)
onDone()
}
const onUploadError = (error) => {
if(error instanceof ErrorFetch && error.status === 400)
setErrorText(`Не удалось импортировать.\n ${error?.message}`)
else
setErrorText(`Не удалось импортировать.`)
}
const getUrl = () => deleteBeforeImport
? url + '/1'
: url + '/0'
return <div>
<p>Загрузить файл excel с операциями на сервер</p>
<span>Очистить список операций перед импортом&nbsp;</span>
<Switch onChange={setDeleteBeforeImport} checked={deleteBeforeImport}/>
<UploadForm
url={getUrl()}
style={{marginTop:'24px'}}
onUploadSuccess={onUploadSuccess}
onUploadError={onUploadError}
/>
<span style={{color:'red', fontWeight:'bold'}}>{errorText}</span>
</div>
}

View File

@ -1,4 +1,4 @@
import ChartDepthToDay from '../../components/charts/ChartDepthToDay';
import ChartTvD from './ChartTvD';
import { useState, useEffect } from 'react';
import { invokeWebApiWrapperAsync } from '../../components/factory';
import { getOperations } from '../Cluster/functions';
@ -7,18 +7,15 @@ import { getOperations } from '../Cluster/functions';
export const Tvd = ({ idWell }) => {
const [dataPlan, setDataPlan] = useState([]);
const [dataFact, setDataFact] = useState([]);
const [dataForecast, setDataForecast] = useState([]);
const [dataPredict, setDataPredict] = useState([]);
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const operations = await getOperations(idWell);
setDataPlan(operations.plan)
setDataFact(operations.fact)
setDataForecast(operations.predict)
setDataPredict(operations.predict)
},
null,
`Не удалось загрузить операции по скважине "${idWell}"`,
@ -29,10 +26,10 @@ export const Tvd = ({ idWell }) => {
<div className="container">
<div>
<h2 className={'mt-20px'}>График Глубина-день</h2>
<ChartDepthToDay
<ChartTvD
dataPlan={dataPlan}
dataFact={dataFact}
dataForecast={dataForecast} />
dataPredict={dataPredict} />
</div>
</div>
);

View File

@ -6,10 +6,8 @@ import {
DatePickerWrapper,
SelectFromDictionary,
makeColumn,
numericColumnOptions,
makeNumericSorter,
makeDateSorter,
defaultNumericRender } from "../../components/Table"
makeNumericColumnOptions} from "../../components/Table"
import LoaderPortal from '../../components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '../../components/factory'
import { WellOperationService} from '../../services/api'
@ -20,20 +18,6 @@ const { TextArea } = Input;
const basePageSize = 160;
const format='YYYY.MM.DD HH:mm'
const numericSortColumnOptions = {
...numericColumnOptions,
sorter: makeNumericSorter('wellDepth'),
render:(value) =>
<div className='text-align-r-container'>
<span>{value}</span>
</div>
}
const durationFormattedColumnOptions = {
...numericColumnOptions,
render: defaultNumericRender
}
export const WellOperationsEditor = ({idWell, idType}) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({current:1, pageSize:basePageSize})
const [paginationTotal, setPaginationTotal] = useState(0)
@ -81,19 +65,20 @@ export const WellOperationsEditor = ({idWell, idType}) => {
render:(_, record)=>getByKeyOrReturnKey(dictionaryOperationCategory, record.idCategory)
}),
makeColumn('Доп. инфо','categoryInfo', {editable:true, width:300, input:<TextArea/>}),
makeColumn('Глубина забоя','wellDepth', numericSortColumnOptions),
makeColumn('Время начала','startDate', {
makeColumn('Глубина забоя на начало','depthStart', makeNumericColumnOptions(2, 'depthStart')),
makeColumn('Глубина забоя при завершении','depthEnd', makeNumericColumnOptions(2, 'depthEnd')),
makeColumn('Время начала','dateStart', {
editable:true,
width:200,
input:<DatePickerWrapper/>,
initialValue:moment().format(),
sorter: makeDateSorter('startDate'),
sorter: makeDateSorter('dateStart'),
render:(_, record) =>
<div className={'text-align-r-container'}>
<span>{moment.utc(record.startDate).local().format(format)}</span>
<span>{moment.utc(record.dateStart).local().format(format)}</span>
</div>
}),
makeColumn('Часы','durationHours', durationFormattedColumnOptions),
makeColumn('Часы','durationHours', makeNumericColumnOptions(2, 'durationHours')),
makeColumn('Комментарий','comment', {editable:true, input:<TextArea/>}),
]

View File

@ -1,17 +1,22 @@
import {Layout, Menu} from "antd";
import {Switch, Link, Route, Redirect, useParams} from "react-router-dom";
import {Switch, Link, Route, Redirect, useParams, useHistory} from "react-router-dom";
import { FolderOutlined } from "@ant-design/icons";
import { WellOperationsEditor } from './WellOperationsEditor'
import { WellSectionsStat } from './WellSectionsStat'
import { Tvd } from './Tvd'
import { WellOpeationsModes } from './WellOpeationsModes'
import { ImportExportBar } from "./ImportExportBar";
const { Content } = Layout
export default function WellOperations({idWell}) {
let {tab} = useParams()
let history = useHistory()
const rootPath = `/well/${idWell}/operations`;
const onImported = () => {
history.push(`${rootPath}`)
}
return(<>
<Menu
mode="horizontal"
@ -33,6 +38,7 @@ export default function WellOperations({idWell}) {
<Menu.Item key={'modes'} icon={<FolderOutlined />}>
<Link to={`${rootPath}/modes`}>Режимы</Link>
</Menu.Item>
<ImportExportBar idWell={idWell} onImported={onImported}/>
</Menu>
<Layout>
<Content className="site-layout-background">
@ -53,7 +59,7 @@ export default function WellOperations({idWell}) {
<WellOpeationsModes idWell={idWell}/>
</Route>
<Route path={rootPath}>
<Redirect to={`${rootPath}/tvd`}/>
<Redirect to={`${rootPath}/plan`}/>
</Route>
</Switch>
</Content>

View File

@ -20,7 +20,7 @@ export type DrillParamsDto = {
topDriveSpeedMin?: number;
topDriveSpeedAvg?: number;
topDriveSpeedMax?: number;
flowMin?: number;
flowAvg?: number;
flowMax?: number;
consumptionMin?: number;
consumptionAvg?: number;
consumptionMax?: number;
}

View File

@ -25,8 +25,8 @@ export class AdminTelemetryService {
* @throws ApiError
*/
public static async mergeTelemetries(
requestBody?: Array<number>,
): Promise<any> {
requestBody?: Array<number>,
): Promise<any> {
const result = await __request({
method: 'POST',
path: `/merge`,

View File

@ -15,10 +15,10 @@ export class DrillParamsService {
* @throws ApiError
*/
public static async getDefaultDrillParams(
idWell?: number,
startDepth?: number,
endDepth?: number,
): Promise<DrillParamsDto> {
idWell?: number,
startDepth?: number,
endDepth?: number,
): Promise<DrillParamsDto> {
const result = await __request({
method: 'GET',
path: `/drillParams/idWell/autoParams`,
@ -39,9 +39,9 @@ export class DrillParamsService {
* @throws ApiError
*/
public static async saveDrillParams(
idWell?: number,
requestBody?: DrillParamsDto,
): Promise<number> {
idWell?: number,
requestBody?: DrillParamsDto,
): Promise<number> {
const result = await __request({
method: 'POST',
path: `/drillParams/idWell`,

View File

@ -149,15 +149,15 @@ export class WellOperationService {
* Импортирует операции из excel (xlsx) файла
* @param idWell id скважины
* @param options Удалить операции перед импортом = 1, если фал валидный
* @param requestBody
* @param requestBody
* @returns any Success
* @throws ApiError
*/
public static async import(
idWell: number,
options: number,
requestBody?: any,
): Promise<any> {
idWell: number,
options: number,
requestBody?: any,
): Promise<any> {
const result = await __request({
method: 'POST',
path: `/api/well/${idWell}/wellOperations/import/${options}`,
@ -173,8 +173,8 @@ export class WellOperationService {
* @throws ApiError
*/
public static async export(
idWell: number,
): Promise<string> {
idWell: number,
): Promise<string> {
const result = await __request({
method: 'GET',
path: `/api/well/${idWell}/wellOperations/export`,
@ -184,13 +184,13 @@ export class WellOperationService {
/**
* Возвращает шаблон файла импорта
* @param idWell
* @param idWell
* @returns string Success
* @throws ApiError
*/
public static async getTamplate(
idWell: string,
): Promise<string> {
idWell: string,
): Promise<string> {
const result = await __request({
method: 'GET',
path: `/api/well/${idWell}/wellOperations/tamplate`,

View File

@ -11,6 +11,10 @@ body {
display: flex;
}
.flex-direction-column {
flex-direction: column;
}
.d-inline {
display: inline;
}
@ -19,10 +23,30 @@ body {
display: none;
}
.w-15 {
width: 15%
}
.w-33 {
width: 33%
}
.w-50 {
width: 50%
}
.w-100 {
width: 100%
}
.m-0 {
margin: 0;
}
.mt-8px {
margin-top: 8px;
}
.mt-20px {
margin-top: 20px;
}
@ -35,6 +59,10 @@ body {
margin-left: 5px;
}
.ml-10px {
margin-left: 10px;
}
.ml-30px {
margin-left: 30px;
}

36
src/styles/measure.css Normal file
View File

@ -0,0 +1,36 @@
input.measure-input {
font-weight:bold;
}
.w-300px {
width: 300px;
}
.measure-buttons-container {
height: 30px;
}
.measure-dates {
height: 160px;
overflow-y: scroll;
}
.measure-button:hover {
cursor: pointer;
}
.selected-timeline {
font-weight: bold;
}
.m-5px {
margin: 5px 5px;
}
.mt-12px {
margin-top: 12px;
}
.ml-10px {
margin-left: 10px;
}