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,12 +1,12 @@
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([
['error', {notifyInstance: notification.error, caption: 'Ошибка'}], ['error' , { notifyInstance: notification.error , caption: 'Ошибка' }],
['warning', {notifyInstance: notification.warning, caption: 'Предупреждение'}], ['warning', { notifyInstance: notification.warning, caption: 'Предупреждение' }],
['info', {notifyInstance: notification.info, caption: 'Инфо'}], ['info' , { notifyInstance: notification.info , caption: 'Инфо' }],
['open', {notifyInstance: notification.info, caption: ''}], ['open' , { notifyInstance: notification.info , caption: '' }],
]) ])
/** /**
@ -14,30 +14,29 @@ const notificationTypeDictionary = new Map([
* @param body string или ReactNode * @param body string или ReactNode
* @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) {
@ -48,49 +47,43 @@ export const invokeWebApiWrapperAsync = async (
notify(errorNotifyText(ex), 'error') notify(errorNotifyText(ex), 'error')
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,
@ -107,28 +100,11 @@ export const downloadFile = async (fileInfo: FileInfoDto) => {
} }
} }
export const formatBytes = (bytes:number) => { export const formatBytes = (bytes: number) => {
if(bytes < 1024) if(bytes < 1024)
return `${bytes.toFixed(0)}b` return `${bytes.toFixed(0)}b`
if(bytes < 1024*1024) if(bytes < 1024 * 1024)
return `${(bytes/1024).toFixed(2)}kb` return `${(bytes / 1024).toFixed(2)}kb`
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 dateColumn = {
title: 'Дата',
key: 'date',
dataIndex: 'date',
render: (date) => formatDate(date, true) ?? 'Нет данных',
fixed: 'left',
width: '8rem',
}
export const InclinometryTable = memo(({ group, visible, onClose }) => {
const [tableColumns, setTableColumns] = useState([]) const [tableColumns, setTableColumns] = useState([])
const [tableData, setTableData] = useState([]) const [tableData, setTableData] = useState([])
useEffect(() => { useEffect(() => setTableColumns([
setTableColumns([{ dateColumn,
title: 'Дата', ...(group?.columns?.map((column) => ({
key: 'date', ...column,
dataIndex: 'date', title: v(column.title)
render: (item) => formatDate(item), })) ?? [])
fixed: 'left', ]), [group?.columns])
width: '8rem',
},
...(group?.columns?.map((column) => ({...column, title: v(column.title)})) ?? [])
])
}, [group?.columns])
useEffect(() => { useEffect(() => setTableData(group?.values?.map(row => ({
setTableData(group?.values?.map(row => ({ date: row.timestamp, ...row.data }))) date: row.timestamp,
}, [group?.values]) ...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])
@ -118,8 +120,8 @@ export const MeasureTable = ({idWell, group, updateMeasuresFunc, additionalButto
<Button key={'edit'} className={'flex-1'} onClick={() => editTable('edit')} disabled={isDataDefault()}> <Button key={'edit'} className={'flex-1'} onClick={() => editTable('edit')} disabled={isDataDefault()}>
<EditOutlined /> <EditOutlined />
</Button> </Button>
<Popconfirm style={{flex: '1'}} title={'Удалить данные?'} onConfirm={markMeasuresAsDeleted} disabled={isDataDefault()}> <Popconfirm style={{ flex: '1' }} title={'Удалить данные?'} onConfirm={markMeasuresAsDeleted} disabled={isDataDefault()}>
<Button key={'delete'} onClick={() => setEditingActionName('delete')} disabled={isDataDefault()} > <Button key={'delete'} onClick={() => setEditingActionName('delete')} disabled={isDataDefault()}>
<DeleteOutlined style={{ margin:'auto 28px' }}/> <DeleteOutlined style={{ margin:'auto 28px' }}/>
</Button> </Button>
</Popconfirm> </Popconfirm>
@ -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,61 +1,50 @@
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} />
} ) : (
<Grid>
{columns.map((column, i) => (
<Fragment key={i}>
<GridItem
key={column.dataIndex}
row={Math.floor(i / colsCount) + 1}
col={(i % colsCount) * 2 + 1}
className={'measure-column-header'}
>
{column.title}
</GridItem>
const valueCellStyle = { <GridItem
border:'1px solid lightgrey', key={column.title}
justifyContent:'right', row={Math.floor(i / colsCount) + 1}
marginRight:'16px', col={(i % colsCount) * 2 + 2}
fontWeight:'bold', className={'measure-column-value'}
textAlign:'right', style={{ padding: 0 }}
padding: 0 >
} {column.render ? (
<Form.Item
export const View = React.memo(({columns, item}) => { key={column.dataIndex}
if (!item || !columns?.length) name={column.dataIndex}
return <Empty key='empty' image={Empty.PRESENTED_IMAGE_SIMPLE}/> style={{ margin: 0 }}
>
return ( {column.render(item[column.dataIndex])}
<Grid> </Form.Item>
{columns.map((column, i) => ( ) : (
<> <p key={column.title} className={'m-5px'}>
<GridItem {item[column.dataIndex]}
key={column.dataIndex} </p>
row={Math.floor(i / colsCount) + 1} )}
col={(i % colsCount) * 2 + 1} </GridItem>
style={headerCellStyle} </Fragment>
> ))}
{column.title} </Grid>
</GridItem> ))
<GridItem
key={column.title}
row={Math.floor(i / colsCount) + 1}
col={(i % colsCount) * 2 + 2}
style={valueCellStyle}
>
{column.render ? (
<Form.Item
key={column.dataIndex}
name={column.dataIndex}
style={{ margin: 0 }}
//rules={column.formItemRules}
>
{column.render(item[column.dataIndex])}
</Form.Item>
) : (
<p key={column.title} className='m-5px'>{item[column.dataIndex]}</p>
)}
</GridItem>
</>
))}
</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,36 +1,37 @@
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 = [
makeColumn('Наименование', 'name', textColumnOptions), makeColumn('Наименование', 'name', textColumnOptions),
makeColumn('Температура, °C', 'temperature',numericColumnOptions), makeColumn('Температура, °C', 'temperature', numericColumnOptions),
makeColumn('Плотность, г/см³', 'density',numericColumnOptions), makeColumn('Плотность, г/см³', 'density', numericColumnOptions),
makeColumn('Усл. вязкость, сек', 'conditionalViscosity',numericColumnOptions), makeColumn('Усл. вязкость, сек', 'conditionalViscosity', numericColumnOptions),
makeColumn('R300', 'r300',numericColumnOptions), makeColumn('R300', 'r300', numericColumnOptions),
makeColumn('R600', 'r600',numericColumnOptions), makeColumn('R600', 'r600', numericColumnOptions),
makeColumn('R3/R6', 'r3r6',numericColumnOptions), makeColumn('R3/R6', 'r3r6', numericColumnOptions),
makeColumn('ДНС, дПа', 'dnsDpa',numericColumnOptions), makeColumn('ДНС, дПа', 'dnsDpa', numericColumnOptions),
makeColumn('Пластич. вязкость, сПз', 'plasticViscocity',numericColumnOptions), makeColumn('Пластич. вязкость, сПз', 'plasticViscocity', numericColumnOptions),
makeColumn('СНС, дПа', 'snsDpa',numericColumnOptions), makeColumn('СНС, дПа', 'snsDpa', numericColumnOptions),
makeColumn('R3/R6 49С', 'r3r649С',numericColumnOptions), makeColumn('R3/R6 49С', 'r3r649С', numericColumnOptions),
makeColumn('ДНС 49С, дПа', 'dns49Cdpa',numericColumnOptions), makeColumn('ДНС 49С, дПа', 'dns49Cdpa', numericColumnOptions),
makeColumn('Пластич. вязкость 49С, сПз', 'plasticViscocity49c',numericColumnOptions), makeColumn('Пластич. вязкость 49С, сПз', 'plasticViscocity49c', numericColumnOptions),
makeColumn('СНС 49С, дПа', 'sns49Cdpa',numericColumnOptions), makeColumn('СНС 49С, дПа', 'sns49Cdpa', numericColumnOptions),
makeColumn('МВТ, кг/м³', 'mbt',numericColumnOptions), makeColumn('МВТ, кг/м³', 'mbt', numericColumnOptions),
makeColumn('Песок, %', 'sand',numericColumnOptions), makeColumn('Песок, %', 'sand', numericColumnOptions),
makeColumn('Фильтрация, см³/30мин', 'filtering',numericColumnOptions), makeColumn('Фильтрация, см³/30мин', 'filtering', numericColumnOptions),
makeColumn('Корка, мм', 'crust',numericColumnOptions), makeColumn('Корка, мм', 'crust', numericColumnOptions),
makeColumn('KTK', 'ktk',numericColumnOptions), makeColumn('KTK', 'ktk', numericColumnOptions),
makeColumn('pH', 'ph',numericColumnOptions), makeColumn('pH', 'ph', numericColumnOptions),
makeColumn('Жесткость, мг/л', 'hardness',numericColumnOptions), makeColumn('Жесткость, мг/л', 'hardness', numericColumnOptions),
makeColumn('Хлориды, мг/л', 'chlorides',numericColumnOptions), makeColumn('Хлориды, мг/л', 'chlorides', numericColumnOptions),
makeColumn('PF', 'pf',numericColumnOptions), makeColumn('PF', 'pf', numericColumnOptions),
makeColumn('Mf', 'mf',numericColumnOptions), makeColumn('Mf', 'mf', numericColumnOptions),
makeColumn('Pm', 'pm',numericColumnOptions), makeColumn('Pm', 'pm', numericColumnOptions),
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,81 +1,83 @@
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 { ReportService } from '../../services/api'
import { invokeWebApiWrapperAsync, downloadFile, formatTimespan} from "../../components/factory" import { formatDate, periodToString } from '../../utils'
import LoaderPortal from "../../components/LoaderPortal" import LoaderPortal from '../../components/LoaderPortal'
import moment from "moment" 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)
}, },
] ]
export const Reports = ({idWell}) => { 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(
const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell) async () => {
const reports = reportsResponse.map(r => ({...r, key: r.id?? r.name?? r.date})) const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell)
setReports(reports) const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date }))
},setShowLoader), [idWell]) setReports(reports)
},
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,82 +69,68 @@ 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" )
const disabledDate = (current) => !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
useEffect(() => invokeWebApiWrapperAsync(
async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(idWell)
if (!datesRangeResponse?.from || !datesRangeResponse.to)
throw new Error('Формат ответа неверный!')
const datesRange = [
moment(datesRangeResponse.from),
moment(datesRangeResponse.to),
]
setAviableDateRange(datesRange)
const from = moment.max(moment(datesRange[1]).subtract(1, 'days'), datesRange[0])
setFilterDateRange([
from.startOf('day'),
moment(datesRange[1]).startOf('day'),
])
},
setShowLoader,
`Не удалось получить диапозон дат рапортов для скважины "${idWell}"`
), [idWell])
useEffect(() => invokeWebApiWrapperAsync(
async () => {
if (filterDateRange?.length !== 2) return
const pagesCount = await ReportService.getReportSize(
idWell,
step,
format,
filterDateRange[0].toISOString(),
filterDateRange[1].toISOString()
) )
console.log(error) setPagesCount(pagesCount)
} },
} setShowLoader,
`Не удалось получить предварительные параметры отчета c
const disabledDate = (current) => { ${filterDateRange[0].format(dateTimeFormat)} по
return current < aviableDateRange[0] || current > aviableDateRange[1] ${filterDateRange[1].format(dateTimeFormat)}`
} ), [filterDateRange, step, format, idWell])
useEffect(
() =>
invokeWebApiWrapperAsync(async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(
idWell
)
const datesRange = [
moment(datesRangeResponse.from),
moment(datesRangeResponse.to),
]
setAviableDateRange(datesRange)
let from = moment(datesRangeResponse.to)
from = from.subtract(1, "days")
if (from < datesRange[0]) from = datesRange[0]
const filterDateDefaults = [
from.startOf("day"),
moment(datesRangeResponse.to).startOf("day"),
]
setFilterDateRange(filterDateDefaults)
}, setShowLoader),
[idWell]
)
useEffect(
() =>
invokeWebApiWrapperAsync(
async () => {
if (!filterDateRange || filterDateRange.length < 2) return
const begin = filterDateRange[0].toISOString()
const end = filterDateRange[1].toISOString()
const pagesCount = await ReportService.getReportSize(
idWell,
step,
format,
begin,
end
)
setPagesCount(pagesCount)
},
setShowLoader,
`Не удалось получить предварительные параметры отчета c
${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format(dateTimeFormat)}`
),
[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) { name: setpointNames.find(spName => spName.value === name)?.label,
setpoints = Object.keys(setpoint?.setpoints).map((name) => ({ value: setpoint?.setpoints?.[name]
name: setpointNames.find(spName => spName.value === name)?.label, }))
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,31 +1,28 @@
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 = [
makeColumn('Конструкция секции','idWellSectionType', { makeColumn('Конструкция секции','idWellSectionType', {
editable:true, editable: true,
input:<SelectFromDictionary dictionary={dictionarySectionType}/>, input: <SelectFromDictionary dictionary={dictionarySectionType}/>,
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 }) => {
const [params, setParams] = useState([]) const [params, setParams] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -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 <EditableTable
await WellOperationService.delete(idWell, operation.id) size={'small'}
updateOperations() bordered
} columns={columns}
dataSource={operations}
return <LoaderPortal show={showLoader}> onRowAdd={makeActionHandler('insertRange', handlerProps, recordParser)}
<EditableTable onRowEdit={makeActionHandler('update', handlerProps, recordParser)}
size='small' onRowDelete={makeActionHandler('delete', handlerProps)}
bordered pagination={{
columns={columns} current: pageNumAndPageSize.current,
dataSource={operations} pageSize: pageNumAndPageSize.pageSize,
onRowAdd={onAdd} showSizeChanger: false,
onRowEdit={onEdit} total: paginationTotal,
onRowDelete={onDelete} onChange: (page, pageSize) => setPageNumAndPageSize({ current: page, pageSize })
pagination={{ }}
current: pageNumAndPageSize.current,
pageSize: pageNumAndPageSize.pageSize,
showSizeChanger: false,
total: paginationTotal,
onChange: (page, pageSize) => setPageNumAndPageSize({current: page, pageSize: 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