* Добавлено таблица редактирования целей в операциях.

* Исправлена ошибка именования в getPermissions
* Обновлено содержимое таблицы на странице операций
This commit is contained in:
goodmice 2022-06-14 17:04:01 +05:00
parent f133728e1b
commit ddecbadfc6
7 changed files with 202 additions and 43 deletions

View File

@ -31,10 +31,6 @@ export type DataType<T = any> = Record<string, T>
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
/*
other - объект с дополнительными свойствами колонки
поддерживаются все базовые свойства из описания https://ant.design/components/table/#Column
плю дополнительные для колонок EditableTable: */
export type columnPropsOther<T = any> = ColumnProps<T> & { export type columnPropsOther<T = any> = ColumnProps<T> & {
// редактируемая колонка // редактируемая колонка
editable?: boolean editable?: boolean

View File

@ -4,16 +4,20 @@ import { Button, Modal } from 'antd'
import { EditableTable, makeTextColumn } from '@components/Table' import { EditableTable, makeTextColumn } from '@components/Table'
import { DrillerService } from '@api' import { DrillerService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const columnOptions = {
editable: true,
const rowClassName = (record) => record.has ? 'driller_list_active' : '' formItemRules: [{ message: 'Обязательное поле!', required: true }]
}
const columns = [ const columns = [
makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, { editable: true, formItemRules: reqRule }), makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, columnOptions),
makeTextColumn('Имя', 'name', undefined, undefined, undefined, { editable: true, formItemRules: reqRule }), makeTextColumn('Имя', 'name', undefined, undefined, undefined, columnOptions),
makeTextColumn('Отчество', 'patronymic', undefined, undefined, undefined, { editable: true }), makeTextColumn('Отчество', 'patronymic', undefined, undefined, undefined, { editable: true }),
] ]
const rowClassName = (record) => record.has ? 'driller_list_active' : ''
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
export const DrillerList = memo(({ loading, drillers, onChange }) => { export const DrillerList = memo(({ loading, drillers, onChange }) => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
@ -26,6 +30,8 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
setShowModal(false) setShowModal(false)
}, []) }, [])
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
const tableHandlers = useMemo(() => { const tableHandlers = useMemo(() => {
const handlerProps = { const handlerProps = {
service: DrillerService, service: DrillerService,
@ -39,7 +45,7 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование бурильщика' }, edit: { ...handlerProps, action: 'update', actionName: 'Редактирование бурильщика' },
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление бурильщика', permission: 'Driller.delete' }, delete: { ...handlerProps, action: 'delete', actionName: 'Удаление бурильщика', permission: 'Driller.delete' },
} }
}, [updateTable]) }, [onChange])
return ( return (
<> <>
@ -55,10 +61,10 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
bordered bordered
size={'small'} size={'small'}
pagination={false} pagination={false}
loading={loading || showLoader} loading={isLoading}
dataSource={drillers} dataSource={drillers}
columns={columns} columns={columns}
scroll={{ y: '75vh', scrollToFirstRowOnChange: true }} scroll={scroll}
onRowAdd={tableHandlers.add} onRowAdd={tableHandlers.add}
onRowEdit={tableHandlers.edit} onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete} onRowDelete={tableHandlers.delete}
@ -67,7 +73,7 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
</Modal> </Modal>
<Button <Button
onClick={onModalOpen} onClick={onModalOpen}
loading={loading || showLoader} loading={isLoading}
style={{ marginRight: '10px' }} style={{ marginRight: '10px' }}
>Список бурильщиков</Button> >Список бурильщиков</Button>
</> </>

View File

@ -10,9 +10,15 @@ import {
makeSelectColumn, makeSelectColumn,
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault } from '@utils'
import { ScheduleService } from '@api' import { ScheduleService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }]
}
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
export const DrillerSchedule = memo(({ drillers, loading, onChange }) => { export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
const [modalVisible, setScheduleModalVisible] = useState(false) const [modalVisible, setScheduleModalVisible] = useState(false)
@ -23,9 +29,12 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
const updateSchedule = useCallback(async () => invokeWebApiWrapperAsync( const updateSchedule = useCallback(async () => invokeWebApiWrapperAsync(
async () => { async () => {
const schedule = await ScheduleService.getByIdWell(idWell) const schedule = arrayOrDefault(await ScheduleService.getByIdWell(idWell))
setSchedule(schedule) setSchedule(schedule)
} },
setShowLoader,
'Не удалось загрузить расписания',
'Получение списка расписаний',
), [idWell]) ), [idWell])
const onModalOpen = useCallback(() => { const onModalOpen = useCallback(() => {
@ -36,12 +45,10 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
setScheduleModalVisible(false) setScheduleModalVisible(false)
}, []) }, [])
useEffect(() => {
updateSchedule()
}, [updateSchedule])
const newScheduleParser = useCallback((record) => ({ ...record, idWell }), [idWell]) const newScheduleParser = useCallback((record) => ({ ...record, idWell }), [idWell])
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
const tableHandlers = useMemo(() => { const tableHandlers = useMemo(() => {
const handlerProps = { const handlerProps = {
service: ScheduleService, service: ScheduleService,
@ -55,7 +62,7 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
return { return {
add: { ...handlerProps, action: 'insert', actionName: 'Добавление расписания', newScheduleParser }, add: { ...handlerProps, action: 'insert', actionName: 'Добавление расписания', newScheduleParser },
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование расписания' }, edit: { ...handlerProps, action: 'update', actionName: 'Редактирование расписания', newScheduleParser },
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление расписания', permission: 'Schedule.delete' }, delete: { ...handlerProps, action: 'delete', actionName: 'Удаление расписания', permission: 'Schedule.delete' },
} }
}, [updateSchedule, newScheduleParser]) }, [updateSchedule, newScheduleParser])
@ -67,25 +74,26 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
})) }))
return [ return [
makeSelectColumn('Бурильщик', 'idDriller', options, undefined, { makeSelectColumn('Бурильщик', 'idDriller', options, undefined, columnOptions, {
editable: true,
formItemRules: reqRule,
}, {
showSearch: true, showSearch: true,
filterOption: (input, option) => filterOption: (input, option) =>
String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0 String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0
}), }),
makeGroupColumn('Рабочий период', [ makeGroupColumn('Рабочий период', [
makeDateColumn('Начало', 'drillStart', undefined, undefined, { editable: true, formItemRules: reqRule }), makeDateColumn('Начало', 'drillStart', undefined, undefined, columnOptions),
makeDateColumn('Конец', 'drillEnd', undefined, undefined, { editable: true, formItemRules: reqRule }), makeDateColumn('Конец', 'drillEnd', undefined, undefined, columnOptions),
]), ]),
makeGroupColumn('Смена', [ makeGroupColumn('Смена', [
makeTimeColumn('Начало', 'shiftStart', undefined, undefined, { editable: true, formItemRules: reqRule }), makeTimeColumn('Начало', 'shiftStart', undefined, undefined, columnOptions),
makeTimeColumn('Конец', 'shiftEnd', undefined, undefined, { editable: true, formItemRules: reqRule }), makeTimeColumn('Конец', 'shiftEnd', undefined, undefined, columnOptions),
]), ]),
] ]
}, [drillers]) }, [drillers])
useEffect(() => {
updateSchedule()
}, [updateSchedule])
return ( return (
<> <>
<Modal <Modal
@ -100,11 +108,11 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
sticky sticky
bordered bordered
size={'small'} size={'small'}
scroll={scroll}
pagination={false} pagination={false}
loading={loading || showLoader} loading={isLoading}
dataSource={schedule} dataSource={schedule}
columns={scheduleColumns} columns={scheduleColumns}
scroll={{ y: '75vh', scrollToFirstRowOnChange: true }}
onRowAdd={tableHandlers.add} onRowAdd={tableHandlers.add}
onRowEdit={tableHandlers.edit} onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete} onRowDelete={tableHandlers.delete}
@ -112,7 +120,7 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
</Modal> </Modal>
<Button <Button
onClick={onModalOpen} onClick={onModalOpen}
loading={loading || showLoader} loading={isLoading}
style={{ marginRight: '10px' }} style={{ marginRight: '10px' }}
>Расписание бурильщиков</Button> >Расписание бурильщиков</Button>
</> </>

View File

@ -1,14 +1,34 @@
import { memo } from 'react' import { memo } from 'react'
import { Table, makeTextColumn, makeNumericColumn, makeDateColumn, makeNumericRender } from '@components/Table' import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table'
import '@styles/detected_operations.less' import '@styles/detected_operations.less'
const numericRender = makeNumericRender(2)
const drillerRender = (driller) => `${driller?.surname ?? ''} ${driller?.name ?? ''} ${driller?.patronymic ?? ''}`
const makeDrillerSorter = (key) => (a, b) => {
const drillerA = a?.[key]
const drillerB = b?.[key]
if (!drillerA && !drillerB) return 0
if (!drillerA) return -1
if (!drillerB) return 1
const strA = drillerRender(drillerA)
const strB = drillerRender(drillerB)
return strA.localeCompare(strB)
}
export const columns = [ export const columns = [
makeTextColumn('Пользователь', 'telemetryUserName', null, null, null, { width: 200 }), makeTextColumn('Бурила', 'driller', null, makeDrillerSorter('driller'), drillerRender, { width: 200 }),
makeDateColumn('Дата начала', 'dateStart', null, undefined, { width: 150 }), makeNumericColumn('Кол-во операций', 'count', null, null, (value) => parseInt(value), 150),
makeNumericColumn('Продолжительность (мин)', 'durationMinutes', null, null, makeNumericRender(2), 150), makeNumericColumn('Среднее по ключевому показателю', 'averageValue', null, null, numericRender, 150),
makeNumericColumn('Глубина (м)', 'depthStart', null, null, makeNumericRender(1), 100), makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', null, null, numericRender, 150),
makeNumericColumn('Эффективность (%)', 'efficiency', null, null, numericRender, 150),
makeNumericColumn('Коэффициент потерь', 'loss', null, null, numericRender, 100),
] ]
export const OperationsTable = memo(({ data, height, ...other }) => ( export const OperationsTable = memo(({ data, height, ...other }) => (

View File

@ -0,0 +1,125 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Modal } from 'antd'
import { useIdWell } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeGroupColumn, makeNumericColumn, makeNumericRender, makeSelectColumn } from '@components/Table'
import { DetectedOperationService, OperationValueService } from '@api'
import { arrayOrDefault } from '@utils'
const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }]
}
const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
const numericRender = makeNumericRender(2)
export const TargetEditor = memo(({ loading }) => {
const [targets, setTargets] = useState([])
const [showModal, setShowModal] = useState(false)
const [showLoader, setShowLoader] = useState(false)
const [targetColumns, setTargetColumns] = useState([])
const idWell = useIdWell()
const updateTable = useCallback(async () => invokeWebApiWrapperAsync(
async () => {
const targets = arrayOrDefault(await OperationValueService.getByIdWell(idWell))
setTargets(targets)
},
setShowLoader,
'Не удалось загрузить цели',
'Получение списка целей',
), [idWell])
const onModalOpen = useCallback(() => {
setShowModal(true)
}, [])
const onModalCancel = useCallback(() => {
setShowModal(false)
}, [])
const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell])
const isLoading = useMemo(() => loading || showLoader, [loading, showLoader])
const tableHandlers = useMemo(() => {
const handlerProps = {
service: OperationValueService,
setLoader: setShowLoader,
onComplete: updateTable,
permission: 'OperationValue.edit',
}
return {
add: { ...handlerProps, action: 'insert', actionName: 'Добавление цели', recordParser },
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование цели', recordParser },
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление цели', permission: 'OperationValue.delete' },
}
}, [updateTable, recordParser])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const categories = arrayOrDefault(await DetectedOperationService.getCategories(idWell))
const options = categories.map(({ id, name }) => ({ value: id, label: name }))
setTargetColumns([
makeSelectColumn('Название', 'idOperationCategory', options, undefined, { ...columnOptions, width: 200 }, {
showSearch: true,
filterOption: (input, option) =>
String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0
}),
makeNumericColumn('Цель', 'targetValue', undefined, undefined, numericRender, 150, columnOptions),
makeNumericColumn('Норм.', 'standardValue', undefined, undefined, numericRender, 150, columnOptions),
makeGroupColumn('Глубина, м', [
makeNumericColumn('Начало', 'depthStart', undefined, undefined, numericRender, 150, columnOptions),
makeNumericColumn('Окончание', 'depthEnd', undefined, undefined, numericRender, 150, columnOptions),
]),
])
},
setShowLoader,
`Не удалось получить список категорий целей`,
'Получение списка категорий целей'
)
}, [idWell])
useEffect(() => {
updateTable()
}, [updateTable])
return (
<>
<Modal
centered
width={1000}
footer={null}
visible={showModal}
onCancel={onModalCancel}
title={'Цели бурения'}
>
<EditableTable
bordered
size={'small'}
scroll={scroll}
pagination={false}
loading={isLoading}
dataSource={targets}
columns={targetColumns}
onRowAdd={tableHandlers.add}
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
/>
</Modal>
<Button
onClick={onModalOpen}
loading={isLoading}
style={{ marginRight: '10px' }}
>Цели</Button>
</>
)
})
export default TargetEditor

View File

@ -6,10 +6,11 @@ import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import { getPermissions, arrayOrDefault, range, wrapPrivateComponent } from '@utils' import { getPermissions, arrayOrDefault, range, wrapPrivateComponent } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import DrillerList from './DrillerList' import DrillerList from './DrillerList'
import TargetEditor from './TargetEditor'
import DrillerSchedule from './DrillerSchedule' import DrillerSchedule from './DrillerSchedule'
import OperationsChart from './OperationsChart' import OperationsChart from './OperationsChart'
import OperationsTable from './OperationsTable' import OperationsTable from './OperationsTable'
@ -21,13 +22,13 @@ const Operations = memo(() => {
const [dateRange, setDateRange] = useState([]) const [dateRange, setDateRange] = useState([])
const [yDomain, setYDomain] = useState(20) const [yDomain, setYDomain] = useState(20)
const [dates, setDates] = useState() const [dates, setDates] = useState()
const [data, setData] = useState([]) const [data, setData] = useState({ operations: [], stats: [] })
const [drillers, setDrillers] = useState([]) const [drillers, setDrillers] = useState([])
const [drillersLoader, setDrillersLoader] = useState(false) const [drillersLoader, setDrillersLoader] = useState(false)
const idWell = useIdWell() const idWell = useIdWell()
const permissions = useMemo(() => getPermissions('Driller.get'), []) const permissions = useMemo(() => getPermissions('Driller.get', 'DetectedOperation.get', 'OperationValue.get'), [])
const disabledDates = useCallback((current) => current && !moment(current).isBetween(...dateRange, 'day', '[]'), [dateRange]) const disabledDates = useCallback((current) => current && !moment(current).isBetween(...dateRange, 'day', '[]'), [dateRange])
@ -108,11 +109,14 @@ const Operations = memo(() => {
<DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} /> <DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} />
</> </>
)} )}
{permissions.detectedOperation.get && permissions.operationValue.get && (
<TargetEditor />
)}
</div> </div>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>
<div className={'page-main'}> <div className={'page-main'}>
<OperationsChart data={data} height={'50vh'} yDomain={yDomain} /> <OperationsChart data={data.operations} height={'50vh'} yDomain={yDomain} />
<OperationsTable data={data} height={'20vh'} /> <OperationsTable data={data.stats} height={'20vh'} />
</div> </div>
</LoaderPortal> </LoaderPortal>
</div> </div>

View File

@ -20,7 +20,7 @@ export function isRequestType(value: string): value is ServiceRequestType {
export const getPermissions = (...values: PermissionRequest[]) => { export const getPermissions = (...values: PermissionRequest[]) => {
const permissions: Record<string, Partial<Record<ServiceRequestType, boolean>>> = {} const permissions: Record<string, Partial<Record<ServiceRequestType, boolean>>> = {}
values.forEach((key) => { values.forEach((key) => {
const [service, type] = key.toLowerCase().split('.') const [service, type] = (key[0].toLowerCase() + key.slice(1)).split('.') // toCamelCase
if (!isRequestType(type)) return if (!isRequestType(type)) return
permissions[service] = permissions[service] ?? {} permissions[service] = permissions[service] ?? {}
permissions[service][type] = hasPermission(key) || (isDev() && getUserLogin() === 'dev') permissions[service][type] = hasPermission(key) || (isDev() && getUserLogin() === 'dev')