diff --git a/src/components/Layout/LayoutPortal.tsx b/src/components/Layout/LayoutPortal.tsx index 438acf9..15de833 100644 --- a/src/components/Layout/LayoutPortal.tsx +++ b/src/components/Layout/LayoutPortal.tsx @@ -2,7 +2,7 @@ import { memo, ReactNode } from 'react' import { Layout, LayoutProps } from 'antd' import PageHeader from '@components/PageHeader' -import WellTreeSelector from '@components/WellTreeSelector' +import WellTreeSelector from '@components/selectors/WellTreeSelector' export type LayoutPortalProps = LayoutProps & { title?: ReactNode diff --git a/src/components/Table/Columns/date.tsx b/src/components/Table/Columns/date.tsx new file mode 100644 index 0000000..c388cf6 --- /dev/null +++ b/src/components/Table/Columns/date.tsx @@ -0,0 +1,25 @@ +import { ReactNode } from 'react' + +import { formatDate } from '@utils' + +import makeColumn, { columnPropsOther } from '.' +import { DatePickerWrapper, makeDateSorter } from '..' + +export const makeDateColumn = ( + title: ReactNode, + key: string, + utc?: boolean, + format?: string, + other?: columnPropsOther +) => makeColumn(title, key, { + ...other, + render: (date) => ( +
+ {formatDate(date, utc, format) ?? '-'} +
+ ), + sorter: makeDateSorter(key), + input: , +}) + +export default makeDateColumn diff --git a/src/components/Table/Columns/index.ts b/src/components/Table/Columns/index.ts index fd0b327..babb1fd 100644 --- a/src/components/Table/Columns/index.ts +++ b/src/components/Table/Columns/index.ts @@ -2,6 +2,7 @@ import { ReactNode } from 'react' import { Rule } from 'antd/lib/form' import { ColumnProps } from 'antd/lib/table' +export { makeDateColumn } from './date' export { RegExpIsFloat, makeNumericRender, diff --git a/src/components/Table/Columns/select.tsx b/src/components/Table/Columns/select.tsx index 85c8e5a..393d6a8 100644 --- a/src/components/Table/Columns/select.tsx +++ b/src/components/Table/Columns/select.tsx @@ -14,8 +14,8 @@ export const makeSelectColumn = ( ...other, input: onChange?.(telemetry?.find((row) => row.id === id))} + className={'telemetry_select'} + dropdownClassName={'telemetry_select'} + {...other} + > + {telemetry?.map((row, i) => ( + + + {getTelemetryLabel(row)} + + + ))} + +)) + +export default TelemetrySelect diff --git a/src/components/WellSelector.jsx b/src/components/selectors/WellSelector.jsx similarity index 100% rename from src/components/WellSelector.jsx rename to src/components/selectors/WellSelector.jsx diff --git a/src/components/WellTreeSelector.tsx b/src/components/selectors/WellTreeSelector.tsx similarity index 96% rename from src/components/WellTreeSelector.tsx rename to src/components/selectors/WellTreeSelector.tsx index 15f96f2..f354d07 100644 --- a/src/components/WellTreeSelector.tsx +++ b/src/components/selectors/WellTreeSelector.tsx @@ -7,9 +7,9 @@ import { RawValueType } from 'rc-tree-select/lib/TreeSelect' import { LabelInValueType } from 'rc-select/lib/Select' import { isRawDate } from '@utils' -import LoaderPortal from './LoaderPortal' -import { WellIcon, WellIconState } from './icons' -import { invokeWebApiWrapperAsync } from './factory' +import LoaderPortal from '@components/LoaderPortal' +import { WellIcon, WellIconState } from '@components/icons' +import { invokeWebApiWrapperAsync } from '@components/factory' import { DepositService, DepositDto } from '@api' import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg' diff --git a/src/components/views/TelemetryView.tsx b/src/components/views/TelemetryView.tsx index 30401ff..702ebd2 100644 --- a/src/components/views/TelemetryView.tsx +++ b/src/components/views/TelemetryView.tsx @@ -4,7 +4,7 @@ import { Tooltip } from 'antd' import { TelemetryDto, TelemetryInfoDto } from '@api' import { Grid, GridItem } from '@components/Grid' -const lables: Record = { +export const lables: Record = { timeZoneId: 'Временная зона', timeZoneOffsetTotalHours: 'Сдвиг временной зоны', drillingStartDate: 'Начало бурения', diff --git a/src/pages/AdminPanel/ClusterController.jsx b/src/pages/AdminPanel/ClusterController.jsx index d513d9d..0da72f5 100644 --- a/src/pages/AdminPanel/ClusterController.jsx +++ b/src/pages/AdminPanel/ClusterController.jsx @@ -1,4 +1,5 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' import { EditableTable, @@ -9,7 +10,6 @@ import { defaultPagination, makeTimezoneColumn } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminClusterService, AdminDepositService } from '@api' import { arrayOrDefault } from '@utils' @@ -22,8 +22,16 @@ export const ClusterController = memo(() => { const [deposits, setDeposits] = useState([]) const [clusters, setClusters] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') - const clusterColumns = [ + const filteredClusters = useMemo(() => clusters.filter((cluster) => cluster && (!searchValue || [ + cluster.caption ?? '', + cluster.latitude?.toString ?? '', + cluster.longitude?.toString() ?? '', + ].join(' ').toLowerCase().includes(searchValue.toLowerCase())) + ), [clusters, searchValue]) + + const clusterColumns = useMemo(() => [ makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { width: 200, editable: true, @@ -38,7 +46,7 @@ export const ClusterController = memo(() => { makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }), makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), - ] + ], [deposits]) const updateTable = useCallback(() => invokeWebApiWrapperAsync( async () => { @@ -63,27 +71,35 @@ export const ClusterController = memo(() => { useEffect(updateTable, [updateTable]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminClusterService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, onComplete: updateTable, - } + }), [updateTable]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/CompanyController.jsx b/src/pages/AdminPanel/CompanyController.jsx index 9973496..836ab11 100644 --- a/src/pages/AdminPanel/CompanyController.jsx +++ b/src/pages/AdminPanel/CompanyController.jsx @@ -1,4 +1,5 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' import { EditableTable, @@ -8,7 +9,6 @@ import { makeSelectColumn, defaultPagination } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminCompanyService, AdminCompanyTypeService } from '@api' import { arrayOrDefault } from '@utils' @@ -20,6 +20,11 @@ export const CompanyController = memo(() => { const [columns, setColumns] = useState([]) const [companies, setCompanies] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') + + const filteredCompanies = useMemo(() => companies.filter((company) => company && (!searchValue || + company.caption?.toLowerCase()?.includes(searchValue.toLowerCase()) + )), [companies, searchValue]) const updateTable = useCallback(async () => { const companies = await AdminCompanyService.getAll() @@ -53,7 +58,7 @@ export const CompanyController = memo(() => { 'Получение списка типов команд' ), [updateTable]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminCompanyService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, @@ -63,22 +68,30 @@ export const CompanyController = memo(() => { `Не удалось обновить список компаний`, 'Получение списка компаний' ), - } + }), [updateTable]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/CompanyTypeController.jsx b/src/pages/AdminPanel/CompanyTypeController.jsx index af55a6a..0fa804d 100644 --- a/src/pages/AdminPanel/CompanyTypeController.jsx +++ b/src/pages/AdminPanel/CompanyTypeController.jsx @@ -1,4 +1,5 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' import { EditableTable, @@ -7,7 +8,6 @@ import { makeStringSorter, defaultPagination } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminCompanyTypeService } from '@api' import { arrayOrDefault } from '@utils' @@ -26,6 +26,11 @@ const columns = [ export const CompanyTypeController = memo(() => { const [companyTypes, setCompanyTypes] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') + + const filteredCompanyTypes = useMemo(() => companyTypes.filter((companyType) => companyType && (!searchValue || + companyType.caption?.toLowerCase()?.includes(searchValue.toLowerCase()) + )), [companyTypes, searchValue]) const updateTable = useCallback(() => invokeWebApiWrapperAsync( async() => { @@ -39,27 +44,35 @@ export const CompanyTypeController = memo(() => { useEffect(updateTable, [updateTable]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminCompanyTypeService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, onComplete: updateTable, - } + }), [updateTable]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/DepositController.jsx b/src/pages/AdminPanel/DepositController.jsx index d033753..9a1b5e1 100644 --- a/src/pages/AdminPanel/DepositController.jsx +++ b/src/pages/AdminPanel/DepositController.jsx @@ -1,12 +1,12 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table' -import { AdminDepositService } from '@api' -import { arrayOrDefault } from '@utils' -import { min1 } from '@utils/validationRules' import { hasPermission } from '@utils/permissions' +import { min1 } from '@utils/validationRules' +import { arrayOrDefault } from '@utils' +import { AdminDepositService } from '@api' export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-' @@ -20,6 +20,14 @@ const depositColumns = [ export const DepositController = memo(() => { const [deposits, setDeposits] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') + + const filteredDeposits = useMemo(() => deposits.filter((deposit) => deposit && (!searchValue || [ + deposit.caption ?? '', + deposit.latitude?.toString() ?? '', + deposit.longitude?.toString() ?? '', + ].join(' ').toLowerCase().includes(searchValue.toLowerCase())) + ), [deposits, searchValue]) const updateTable = useCallback(() => invokeWebApiWrapperAsync( async() => { @@ -33,27 +41,35 @@ export const DepositController = memo(() => { useEffect(updateTable, [updateTable]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminDepositService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, onComplete: updateTable, - } + }), [updateTable]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/PermissionController.jsx b/src/pages/AdminPanel/PermissionController.jsx index a094e60..8f1ecba 100644 --- a/src/pages/AdminPanel/PermissionController.jsx +++ b/src/pages/AdminPanel/PermissionController.jsx @@ -1,4 +1,5 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' import { EditableTable, @@ -6,7 +7,6 @@ import { makeColumn, makeStringSorter } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminPermissionService } from '@api' import { arrayOrDefault } from '@utils' @@ -27,8 +27,15 @@ const columns = [ ] export const PermissionController = memo(() => { - const [showLoader, setShowLoader] = useState(false) const [permissions, setPermissions] = useState([]) + const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') + + const filteredPermissions = useMemo(() => permissions.filter((permission) => permission && (!searchValue || [ + permission.name ?? '', + permission.description ?? '', + ].join(' ').includes(searchValue.toLowerCase())) + ), [permissions, searchValue]) const updateTable = useCallback(async () => invokeWebApiWrapperAsync( async () => { @@ -42,27 +49,35 @@ export const PermissionController = memo(() => { useEffect(() => updateTable(), [updateTable]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminPermissionService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, onComplete: updateTable - } + }), [updateTable]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/RoleController.jsx b/src/pages/AdminPanel/RoleController.jsx index 63e2d5b..102cf65 100644 --- a/src/pages/AdminPanel/RoleController.jsx +++ b/src/pages/AdminPanel/RoleController.jsx @@ -1,9 +1,9 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input } from 'antd' -import LoaderPortal from '@components/LoaderPortal' import { PermissionView, RoleView } from '@components/views' import { invokeWebApiWrapperAsync } from '@components/factory' -import { EditableTable, makeActionHandler, makeColumn, makeTagColumn } from '@components/Table' +import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table' import { AdminPermissionService, AdminUserRoleService } from '@api' import { arrayOrDefault } from '@utils' import { min1 } from '@utils/validationRules' @@ -13,29 +13,31 @@ export const RoleController = memo(() => { const [permissions, setPermissions] = useState([]) const [roles, setRoles] = useState([]) const [showLoader, setShowLoader] = useState(false) - const [columns, setColumns] = useState([]) + const [searchValue, setSearchValue] = useState('') + + const filteredRoles = useMemo(() => roles.filter((role) => role && (!searchValue || + role.caption?.toLowerCase()?.includes(searchValue.toLowerCase()) + )), [roles, searchValue]) + + const columns = useMemo(() => [ + makeTextColumn('Название', 'caption', null, null, null, { width: 100, editable: true, formItemRules: min1 }), + makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { + width: 400, + editable: true, + render: (role) => + }, { allowClear: true }), + makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { + width: 600, + editable: true, + render: (permission) => , + }), + ], [roles, permissions]) const loadRoles = useCallback(async () => { const roles = await AdminUserRoleService.getAll() setRoles(arrayOrDefault(roles)) }, []) - useEffect(() => { - setColumns([ - makeColumn('Название', 'caption', { width: 100, editable: true, formItemRules: min1 }), - makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { - width: 400, - editable: true, - render: (role) => - }, { allowClear: true }), - makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { - width: 600, - editable: true, - render: (permission) => , - }), - ]) - }, [roles, permissions]) - useEffect(() => invokeWebApiWrapperAsync( async () => { const permissions = await AdminPermissionService.getAll() @@ -47,7 +49,7 @@ export const RoleController = memo(() => { 'Получение списка ролей' ), [loadRoles]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: AdminUserRoleService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, @@ -55,23 +57,31 @@ export const RoleController = memo(() => { loadRoles, setShowLoader, `Не удалось загрузить список ролей`, - 'Получение списка ролей' + 'Получение списка ролей', ) - } + }), [loadRoles]) return ( - + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> - + ) }) diff --git a/src/pages/AdminPanel/Telemetry/TelemetryMerger.jsx b/src/pages/AdminPanel/Telemetry/TelemetryMerger.jsx new file mode 100644 index 0000000..31991d9 --- /dev/null +++ b/src/pages/AdminPanel/Telemetry/TelemetryMerger.jsx @@ -0,0 +1,136 @@ +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { PullRequestOutlined } from '@ant-design/icons' +import { Button, Descriptions, Popconfirm } from 'antd' + +import { Grid, GridItem } from '@components/Grid' +import LoaderPortal from '@components/LoaderPortal' +import { lables } from '@components/views/TelemetryView' +import { invokeWebApiWrapperAsync } from '@components/factory' +import TelemetrySelect from '@components/selectors/TelemetrySelect' +import { AdminTelemetryService } from '@api' +import { arrayOrDefault } from '@utils' + +const { Item } = Descriptions + +export const TelemetryInfo = memo(({ info, danger, ...other }) => ( + + {Object.keys({ ...lables, ...info }).map(key => ( + {info?.[key] ?? '-'} + ))} + +)) + +export const TelemetryMerger = memo(() => { + const [primary, setPrimary] = useState(null) + const [secondary, setSecondary] = useState(null) + const [telemetry, setTelemetry] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [isMerging, setIsMerging] = useState(false) + + const location = useLocation() + + const danger = useMemo(() => [ + primary?.info?.well !== secondary?.info?.well && 'well', + primary?.info?.cluster !== secondary?.info?.cluster && 'cluster', + primary?.info?.deposit !== secondary?.info?.deposit && 'deposit', + ], [primary, secondary]) + + const updateTelemetry = useCallback(async () => await invokeWebApiWrapperAsync( + async () => { + const telemetry = arrayOrDefault(await AdminTelemetryService.getAll()) + setTelemetry(telemetry) + }, + setIsLoading, + 'Не удалось загрузить список телеметрий', + 'Получение списка телеметрий', + ), []) + + const mergeTelemetry = useCallback(() => invokeWebApiWrapperAsync( + async () => { + await new Promise(res => setTimeout(res, 1000)) + /// await AdminTelemetryService.mergeTelemetries(secondary.id, primary.id) + await updateTelemetry() + }, + setIsMerging, + 'Не удалось объединить телеметрии', + 'Объединение телеметрий', + ), [updateTelemetry]) + + useEffect(() => updateTelemetry(), [updateTelemetry]) + + useEffect(() => { + const query = new URLSearchParams(location.search) + const primaryId = parseInt(query.get('primary') ?? null) + const secondaryId = parseInt(query.get('secondary') ?? null) + const primary = isNaN(primaryId) ? null : telemetry.find((t) => t.id === primaryId) + const secondary = isNaN(secondaryId) ? null : telemetry.find((t) => t.id === secondaryId) + console.log([primary, secondary]) + setPrimary(primary) + setSecondary(secondary) + }, [location, telemetry]) + + return ( + +
{ + ///TODO: Добавить описание + }
+ + Результирующая телеметрия + Исходная телеметрия + + + + + + + + + + + + + + + + + + +
+ ) +}) + +export default TelemetryMerger diff --git a/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx new file mode 100644 index 0000000..b02c122 --- /dev/null +++ b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx @@ -0,0 +1,121 @@ +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { PullRequestOutlined } from '@ant-design/icons' +import { Button, Input } from 'antd' + +import { + defaultPagination, + makeColumn, + makeDateSorter, + makeNumericColumn, + makeNumericRender, + makeTextColumn, + Table +} from '@components/Table' +import Poprompt from '@components/selectors/Poprompt' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { AdminTelemetryService } from '@api' +import { arrayOrDefault } from '@utils' +import { useHistory } from 'react-router-dom' + +export const TelemetryController = memo(() => { + const [telemetryData, setTelemetryData] = useState([]) + const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') + + const history = useHistory() + + const toMerger = useCallback((type, id) => () => history.push(`/admin/telemetry/merger/?${type}=${id}`), [history]) + + const mergeRender = useCallback((value, record) => ( + , + size: 'small', + danger: !!value, + }} + footer={( +
+ + +
+ )} + > +

Вы собираетесь использовать данную телеметрию для слияния

+ {record.realWell && ( +

Внимание! Телеметрии назначена скважина!

+ )} +
+ ), [toMerger]) + + const columns = useMemo(() => [ + makeColumn('', 'hasParent', { render: mergeRender }), + makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), + makeTextColumn('UID', 'remoteUid'), + makeTextColumn('Назначена на скважину', 'realWell'), + makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')), + makeTextColumn('Часовой пояс', 'timeZoneId'), + makeTextColumn('Скважина', 'well'), + makeTextColumn('Куст', 'cluster'), + makeTextColumn('Месторождение', 'deposit'), + makeTextColumn('Заказчик', 'customer'), + makeTextColumn('Комментарий', 'comment'), + makeTextColumn('Версия HMI', 'hmiVersion'), + makeTextColumn('Версия САУБ', 'saubPlcVersion'), + makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'), + ], [mergeRender]) + + const filteredTelemetryData = useMemo(() => telemetryData.filter((telemetry) => telemetry && (!searchValue || [ + telemetry.id?.toString() ?? '', + telemetry.remoteUid ?? '', + telemetry.realWell ?? '', + telemetry.drillingStartDate ?? '', + telemetry.well ?? '', + telemetry.cluster ?? '', + telemetry.deposit ?? '', + telemetry.customer ?? '', + telemetry.comment ?? '', + telemetry.hmiVersion ?? '', + telemetry.saubPlcVersion ?? '', + telemetry.spinPlcVersion ?? '', + ].join(' ').toLowerCase().includes(searchValue.toLowerCase())) + ), [telemetryData, searchValue]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll()) + setTelemetryData(telemetryData.map((telemetry) => ({ + ...(telemetry?.info ?? []), + id: telemetry?.id, + remoteUid: telemetry?.remoteUid, + realWell: telemetry?.well?.caption, + }))) + }, + setShowLoader, + `Не удалось загрузить список телеметрии скважин`, + 'Полученик списка телеметрии скважин' + ), []) + + return ( + <> + setSearchValue(e.target.value)} + value={searchValue} + loading={showLoader} + /> + + + ) +}) + +export default TelemetryController diff --git a/src/pages/AdminPanel/Telemetry/index.jsx b/src/pages/AdminPanel/Telemetry/index.jsx new file mode 100644 index 0000000..0db46d6 --- /dev/null +++ b/src/pages/AdminPanel/Telemetry/index.jsx @@ -0,0 +1,42 @@ +import { Layout, Menu } from 'antd' +import { lazy, memo, Suspense } from 'react' +import { Switch, useParams } from 'react-router-dom' + +import { PrivateMenuItem, PrivateRoute, PrivateDefaultRoute } from '@components/Private' + +import { SuspenseFallback } from '@pages/SuspenseFallback' + +const TelemetryViewer = lazy(() => import('./TelemetryViewer')) +const TelemetryMerger = lazy(() => import('./TelemetryMerger')) + +const rootPath = '/admin/telemetry' + +export const Telemetry = memo(() => { + const { tab } = useParams() + + return ( + + + + + + + + + }> + + + + + + + + + + ) +}) + +export default Telemetry diff --git a/src/pages/AdminPanel/TelemetryController.jsx b/src/pages/AdminPanel/TelemetryController.jsx deleted file mode 100644 index 7621ce9..0000000 --- a/src/pages/AdminPanel/TelemetryController.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import { memo, useEffect, useState } from 'react' - -import { - defaultPagination, - makeDateSorter, - makeNumericColumn, - makeNumericRender, - makeTextColumn, - Table -} from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' -import { invokeWebApiWrapperAsync } from '@components/factory' -import { AdminTelemetryService } from '@api' -import { arrayOrDefault } from '@utils' - -const columns = [ - makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), - makeTextColumn('UID', 'remoteUid'), - makeTextColumn('Назначена на скважину', 'realWell'), - makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')), - makeTextColumn('Часовой пояс', 'timeZoneId'), - makeTextColumn('Скважина', 'well'), - makeTextColumn('Куст', 'cluster'), - makeTextColumn('Месторождение', 'deposit'), - makeTextColumn('Заказчик', 'customer'), - makeTextColumn('Комментарий', 'comment'), - makeTextColumn('Версия HMI', 'hmiVersion'), - makeTextColumn('Версия САУБ', 'saubPlcVersion'), - makeTextColumn('Версия Спин Мастер', 'spinPlcVersion'), -] - -export const TelemetryController = memo(() => { - const [telemetryData, setTelemetryData] = useState([]) - const [isLoading, setIsLoading] = useState(false) - - useEffect(() => invokeWebApiWrapperAsync( - async () => { - const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll()) - setTelemetryData(telemetryData.map((telemetry) => ({ - ...(telemetry?.info ?? []), - id: telemetry?.id, - remoteUid: telemetry?.remoteUid, - realWell: telemetry?.well?.caption, - }))) - }, - setIsLoading, - `Не удалось загрузить список телеметрии скважин`, - 'Полученик списка телеметрии скважин' - ), []) - - return ( - -
- - ) -}) - -export default TelemetryController diff --git a/src/pages/AdminPanel/UserController/RoleTag.jsx b/src/pages/AdminPanel/UserController/RoleTag.jsx index c67aa26..a88c7ce 100644 --- a/src/pages/AdminPanel/UserController/RoleTag.jsx +++ b/src/pages/AdminPanel/UserController/RoleTag.jsx @@ -1,16 +1,12 @@ import { Select } from 'antd' -import { memo, useEffect, useState } from 'react' +import { memo, useMemo } from 'react' export const RoleTag = memo(({ roles, value, onChange }) => { - const [options, setOptions] = useState([]) - - useEffect(() => { - setOptions(roles.map((elm) => ({ - key: Date.now(), - value: `${elm.caption}`, - label: elm.caption - }))) - }, [roles]) + const options = useMemo(() => roles.map((elm) => ({ + key: Date.now(), + value: `${elm.caption}`, + label: elm.caption + })), [roles]) return (
- + ) }) diff --git a/src/pages/AdminPanel/WellController/TelemetrySelect.jsx b/src/pages/AdminPanel/WellController/TelemetrySelect.jsx deleted file mode 100644 index f33484e..0000000 --- a/src/pages/AdminPanel/WellController/TelemetrySelect.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { memo } from 'react' -import { Select } from 'antd' - -import { getTelemetryLabel } from '@components/views' - -export const TelemetrySelect = memo(({ telemetry, value, onChange }) => ( - -)) - -export default TelemetrySelect diff --git a/src/pages/AdminPanel/WellController/index.jsx b/src/pages/AdminPanel/WellController/index.jsx index 97eda0c..5edd559 100644 --- a/src/pages/AdminPanel/WellController/index.jsx +++ b/src/pages/AdminPanel/WellController/index.jsx @@ -1,6 +1,6 @@ -import { Button } from 'antd' +import { Button, Input } from 'antd' import { CopyOutlined } from '@ant-design/icons' -import { memo, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { AdminClusterService, @@ -19,16 +19,13 @@ import { defaultPagination, makeTimezoneColumn, } from '@components/Table' -import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { TelemetryView, CompanyView } from '@components/views' +import TelemetrySelect from '@components/selectors/TelemetrySelect' import { hasPermission } from '@utils/permissions' import { arrayOrDefault } from '@utils' import { coordsFixed } from '../DepositController' -import TelemetrySelect from './TelemetrySelect' - -import '@styles/admin.css' const wellTypes = [ { value: 1, label: 'Наклонно-направленная' }, @@ -44,8 +41,17 @@ export const WellController = memo(() => { const [columns, setColumns] = useState([]) const [wells, setWells] = useState([]) const [showLoader, setShowLoader] = useState(false) + const [searchValue, setSearchValue] = useState('') - const updateTable = async () => invokeWebApiWrapperAsync( + const filteredWells = useMemo(() => wells.filter((well) => well && (!searchValue || [ + well.caption ?? '', + well.latitude?.toString() ?? '', + well.longitude?.toString() ?? '', + ].join(' ').toLowerCase().includes(searchValue.toLowerCase())) + ), [wells, searchValue]) + + + const updateTable = useCallback(async () => invokeWebApiWrapperAsync( async () => { const wells = await AdminWellService.getAll() setWells(arrayOrDefault(wells)) @@ -53,11 +59,11 @@ export const WellController = memo(() => { setShowLoader, `Не удалось загрузить список скважин`, 'Получение списка скважин' - ) + ), []) - const duplicateWell = (well) => { + const duplicateWell = useCallback((well) => { // TODO: Метод дубликации скважины - } + }, []) const addititonalButtons = memo((record, editingKey) => (
{ tableName={'well_operations'} /> ) -} +}) export default WellOperationsTable diff --git a/src/pages/Cluster/index.jsx b/src/pages/Cluster/index.jsx index 0b334b9..99e060f 100644 --- a/src/pages/Cluster/index.jsx +++ b/src/pages/Cluster/index.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, memo } from 'react' import { useParams } from 'react-router-dom' import { arrayOrDefault } from '@utils' @@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import ClusterWells from './ClusterWells' -export const Cluster = () => { +export const Cluster = memo(() => { const { idCluster } = useParams() const [data, setData] = useState([]) const [showLoader, setShowLoader] = useState(false) @@ -28,6 +28,6 @@ export const Cluster = () => { ) -} +}) export default Cluster diff --git a/src/pages/Documents/DocumentsTemplate.jsx b/src/pages/Documents/DocumentsTemplate.jsx index d25ee42..f786551 100644 --- a/src/pages/Documents/DocumentsTemplate.jsx +++ b/src/pages/Documents/DocumentsTemplate.jsx @@ -1,14 +1,13 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo, useCallback } from 'react' import { DatePicker, Button, Input } from 'antd' import { FileService } from '@api' -import { hasPermission } from '@utils/permissions' import LoaderPortal from '@components/LoaderPortal' import { UploadForm } from '@components/UploadForm' import { CompanyView, UserView } from '@components/views' -import { EditableTable, makePaginationObject } from '@components/Table' +import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table' import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory' -import { formatDate } from '@utils' +import { hasPermission } from '@utils/permissions' const pageSize = 12 const { RangePicker } = DatePicker @@ -24,26 +23,11 @@ const columns = [ {name} ), - }, { - title: 'Дата загрузки', - key: 'uploadDate', - dataIndex: 'uploadDate', - render: item => formatDate(item, false, 'DD MMM YYYY, HH:mm:ss'), - }, { - title: 'Размер', - key: 'size', - dataIndex: 'size', - render: item => formatBytes(item) - }, { - title: 'Автор', - key: 'author', - dataIndex: 'author', - render: item => - }, { - title: 'Компания', - key: 'company', - render: (_, record) => - } + }, + makeDateColumn('Дата загрузки', 'uploadDate'), + makeNumericColumn('Размер', 'size', null, null, formatBytes), + makeColumn('Автор', 'author', { render: item => }), + makeColumn('Компания', 'company', { render: (_, record) => }) ] export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, customColumns, beforeTable, onChange, tableName }) => { @@ -55,21 +39,13 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus const [files, setFiles] = useState([]) const [showLoader, setShowLoader] = useState(false) - const uploadUrl = `/api/well/${idWell}/files/?idCategory=${idCategory}` + const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory]) - const handleUploadComplete = () => update() + const mergedColumns = useMemo(() => [...columns, ...(customColumns ?? [])], [customColumns]) + const companies = useMemo(() => [...new Set(files.map(file => file.company))].filter(company => company), [files]) + const filenames = useMemo(() => [...new Set(files.map(file => file.name))].filter(name => name), [files]) - const handleFileDelete = async (file) => { - await FileService.delete(idWell, file.id) - update() - } - - const hanleCompanySearch = (value, _) => setFilterCompanyName(value) - const hanleFileNameSearch = (value, _) => setFilterFileName(value) - - const mergedColumns = [...columns, ...(customColumns ?? [])] - - const update = () => { + const update = useCallback(() => { let begin = null let end = null if (filterDataRange?.length > 1) { @@ -101,13 +77,15 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus `Не удалось загрузить файлы по скважине "${idWell}"`, 'Загрузка файла по скважине' ) - } + }, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page]) - useEffect(update, [idWell, idCategory, page, filterDataRange, filterCompanyName, filterFileName]) + useEffect(update, [update]) useEffect(() => onChange?.(files), [files, onChange]) - const companies = [...new Set(files.map(file => file.company))].filter(company => company) - const filenames = [...new Set(files.map(file => file.name))].filter(name => name) + const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => { + await FileService.delete(idWell, file.id) + update() + }), [idWell, idCategory, update]) return ( @@ -124,7 +102,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus {companies.map((company) => ( @@ -138,7 +116,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus {filenames.map((name) => ( @@ -154,7 +132,7 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus url={uploadUrl} accept={accept} onUploadStart={() => setShowLoader(true)} - onUploadComplete={handleUploadComplete} + onUploadComplete={update} /> )} @@ -169,9 +147,9 @@ export const DocumentsTemplate = ({ idCategory, idWell, accept, headerChild, cus pagination={{ ...pagination, showSizeChanger: false, - onChange: (page) => setPage(page), + onChange: setPage, }} - onRowDelete={hasPermission(`File.edit${idCategory}`) && handleFileDelete} + onRowDelete={handleFileDelete} rowKey={(record) => record.id} tableName={tableName ?? `file_${idCategory}`} /> diff --git a/src/pages/Documents/index.jsx b/src/pages/Documents/index.jsx index 1d79c0e..5452d63 100644 --- a/src/pages/Documents/index.jsx +++ b/src/pages/Documents/index.jsx @@ -1,5 +1,5 @@ import { join } from 'path' -import { memo } from 'react' +import { memo, useMemo } from 'react' import { Layout, Menu } from 'antd' import { FolderOutlined } from '@ant-design/icons' import { Switch, useParams } from 'react-router-dom' @@ -25,7 +25,7 @@ export const documentCategories = [ export const MenuDocuments = memo(({ idWell }) => { const { category } = useParams() - const root = `/well/${idWell}/document` + const root = useMemo(() => `/well/${idWell}/document`, [idWell]) return ( <> diff --git a/src/pages/DrillingProgram/CategoryAdder.jsx b/src/pages/DrillingProgram/CategoryAdder.jsx index 390f8f9..dd45dad 100644 --- a/src/pages/DrillingProgram/CategoryAdder.jsx +++ b/src/pages/DrillingProgram/CategoryAdder.jsx @@ -2,7 +2,7 @@ import { Form, Select } from 'antd' import { FileAddOutlined } from '@ant-design/icons' import { memo, useCallback, useEffect, useState } from 'react' -import Poprompt from '@components/Poprompt' +import Poprompt from '@components/selectors/Poprompt' import { invokeWebApiWrapperAsync } from '@components/factory' import { DrillingProgramService } from '@api' diff --git a/src/pages/DrillingProgram/CategoryEditor.jsx b/src/pages/DrillingProgram/CategoryEditor.jsx index 15300a5..a9fedec 100644 --- a/src/pages/DrillingProgram/CategoryEditor.jsx +++ b/src/pages/DrillingProgram/CategoryEditor.jsx @@ -1,5 +1,5 @@ import { Input, Modal, Radio } from 'antd' -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs' import { UserView } from '@components/views' @@ -113,7 +113,7 @@ export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) => `Изменение статуса пользователя` ), [users, idWell, category.idFileCategory]) - const userColumns = [ + const userColumns = useMemo(() => [ makeColumn('Пользователь', 'user', { sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0, render: (user) => , @@ -129,7 +129,7 @@ export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) => /> ), }) - ] + ], [changeUserStatus]) const onSearchTextChange = useCallback((e) => subject?.next(e?.target?.value), [subject]) diff --git a/src/pages/DrillingProgram/CategoryRender.jsx b/src/pages/DrillingProgram/CategoryRender.jsx index 7a00050..50316be 100644 --- a/src/pages/DrillingProgram/CategoryRender.jsx +++ b/src/pages/DrillingProgram/CategoryRender.jsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { Button, Input, Popconfirm, Form } from 'antd' import { DeleteOutlined, @@ -6,11 +6,11 @@ import { TableOutlined, } from '@ant-design/icons' -import Poprompt from '@components/Poprompt' import { UserView } from '@components/views' import UploadForm from '@components/UploadForm' import DownloadLink from '@components/DownloadLink' import LoaderPortal from '@components/LoaderPortal' +import Poprompt from '@components/selectors/Poprompt' import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory' import { DrillingProgramService } from '@api' import { formatDate } from '@utils' @@ -44,7 +44,7 @@ export const CategoryRender = memo(({ idWell, partData, onUpdate, onEdit, onHist file // Информация о файле } = partData ?? {} - const uploadUrl = `/api/well/${idWell}/drillingProgram/part/${idFileCategory}` + const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory]) const [isUploading, setIsUploading] = useState(false) const [isDeleting, setIsDeleting] = useState(false) diff --git a/src/pages/DrillingProgram/index.jsx b/src/pages/DrillingProgram/index.jsx index 2e78b81..303d240 100644 --- a/src/pages/DrillingProgram/index.jsx +++ b/src/pages/DrillingProgram/index.jsx @@ -8,7 +8,7 @@ import { ReloadOutlined, WarningOutlined, } from '@ant-design/icons' -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import LoaderPortal from '@components/LoaderPortal' import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory' @@ -52,11 +52,11 @@ export const DrillingProgram = memo(({ idWell }) => { parts, program, error, - } = data + } = useMemo(() => data, [data]) - const stateId = idState ?? idStateUnknown - const state = stateString[stateId] - const StateIcon = state.icon + const stateId = useMemo(() => idState ?? idStateUnknown, [idState]) + const state = useMemo(() => stateString[stateId], [stateId]) + const StateIcon = useMemo(() => state.icon, [state?.icon]) const updateData = useCallback(async () => await invokeWebApiWrapperAsync( async () => { @@ -76,15 +76,15 @@ export const DrillingProgram = memo(({ idWell }) => { useEffect(() => updateData(), [updateData]) - const onCategoryEdit = (catId) => { + const onCategoryEdit = useCallback((catId) => { setSelectedCategory(catId) setEditorVisible(!!catId) - } + }, []) - const onCategoryHistory = (catId) => { + const onCategoryHistory = useCallback((catId) => { setSelectedCategory(catId) setHistoryVisible(!!catId) - } + }, []) const onEditorClosed = useCallback(() => { setEditorVisible(false) diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx index 9015588..ff53576 100644 --- a/src/pages/Login.jsx +++ b/src/pages/Login.jsx @@ -13,9 +13,10 @@ import '@styles/index.css' import Logo from '@images/Logo' export const Login = memo(() => { + const [showLoader, setShowLoader] = useState(false) + const history = useHistory() const location = useLocation() - const [showLoader, setShowLoader] = useState(false) const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync( async () => { diff --git a/src/pages/Measure/InclinometryTable.jsx b/src/pages/Measure/InclinometryTable.jsx index 7772cfc..18d1660 100644 --- a/src/pages/Measure/InclinometryTable.jsx +++ b/src/pages/Measure/InclinometryTable.jsx @@ -1,5 +1,5 @@ import { Modal } from 'antd' -import { memo, useEffect, useState } from 'react' +import { memo, useMemo } from 'react' import { Table } from '@components/Table' import { formatDate } from '@utils' @@ -18,21 +18,18 @@ const dateColumn = { } export const InclinometryTable = memo(({ group, visible, onClose }) => { - const [tableColumns, setTableColumns] = useState([]) - const [tableData, setTableData] = useState([]) - - useEffect(() => setTableColumns([ + const tableColumns = useMemo(() => [ dateColumn, ...(group?.columns?.map((column) => ({ ...column, title: v(column.title) })) ?? []) - ]), [group?.columns]) + ], [group?.columns]) - useEffect(() => setTableData(group?.values?.map(row => ({ + const tableData = useMemo(() => group?.values?.map(row => ({ date: row.timestamp, ...row.data - }))), [group?.values]) + })), [group?.values]) return !group?.columns ? null : ( cols.map(col => ({ render: renderDelegate, ...col })) +const disabled = !hasPermission('Measure.edit') + export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additionalButtons }) => { const [showLoader, setShowLoader] = useState(false) const [displayedValues, setDisplayedValues] = useState({}) const [editingColumns, setEditingColumns] = useState(group.columns) const [isTableEditing, setIsTableEditing] = useState(false) const [editingActionName, setEditingActionName] = useState('') - const [data, setData] = useState([]) const [measuresForm] = Form.useForm() - useEffect(() => { - let data = [group.defaultValue] - if (group?.values?.length > 0) - data = group.values - setData(data) - setDisplayedValues(data.at(-1)) - }, [group.defaultValue, group.values]) + const data = useMemo(() => group?.values?.length > 0 ? group.values : [group?.defaultValue], [group?.defaultValue, group?.values]) + + useEffect(() => setDisplayedValues(data.at(-1)), [data]) useEffect(() => { const switchableColumns = createEditingColumns( @@ -64,17 +61,15 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona `Не удалось удалить запись ${displayedValues.id} для скважины "${idWell}"`, 'Удаление записи для скважины' ) + const editingDisabled = useMemo(() => disabled || !!displayedValues?.isDefaultData, [displayedValues?.isDefaultData]) + const deleteDisabled = useMemo(() => !hasPermission('Measure.delete') || !!displayedValues?.isDefaultData, [displayedValues?.isDefaultData]) - const disabled = !hasPermission('Measure.edit') - const editingDisabled = disabled || !!displayedValues?.isDefaultData - const deleteDisabled = !hasPermission('Measure.delete') || !!displayedValues?.isDefaultData - - const editTable = (action) => { + const editTable = useCallback((action) => { setEditingActionName(action) setIsTableEditing(true) - } + }, []) - const handleSubmitMeasuresForm = async (formData) => await invokeWebApiWrapperAsync( + const handleSubmitMeasuresForm = useCallback(async (formData) => await invokeWebApiWrapperAsync( async () => { measuresForm.validateFields() @@ -99,7 +94,7 @@ export const MeasureTable = memo(({ idWell, group, updateMeasuresFunc, additiona setShowLoader, `Не удалось добавить/изменить запись для скаважины "${idWell}"`, 'Добавление/изменение записи по скважине' - ) + ), [displayedValues?.id, displayedValues?.timestamp, editingActionName, group.idCategory, idWell, measuresForm, updateMeasuresFunc]) return ( <> diff --git a/src/pages/Messages.jsx b/src/pages/Messages.jsx index e4bd773..2500ada 100644 --- a/src/pages/Messages.jsx +++ b/src/pages/Messages.jsx @@ -1,5 +1,5 @@ import moment from 'moment' -import { useState, useEffect, memo } from 'react' +import { useState, useEffect, memo, useCallback } from 'react' import { Table, Select, DatePicker, Input } from 'antd' import { MessageService } from '@api' @@ -65,6 +65,8 @@ const filterOptions = [ { value: 3, label: 'Информация' }, ] +const children = filterOptions.map((line) => ) + // Данные для таблицы export const Messages = memo(({ idWell }) => { const [messages, setMessages] = useState([]) @@ -75,9 +77,7 @@ export const Messages = memo(({ idWell }) => { const [searchString, setSearchString] = useState('') const [showLoader, setShowLoader] = useState(false) - const children = filterOptions.map((line) => ) - - const onChangeSearchString = (message) => setSearchString(message.length > 2 ? message : '') + const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), []) useEffect(() => invokeWebApiWrapperAsync( async () => { diff --git a/src/pages/Report/index.jsx b/src/pages/Report/index.jsx index 3fb7917..ce8a0d4 100644 --- a/src/pages/Report/index.jsx +++ b/src/pages/Report/index.jsx @@ -1,6 +1,6 @@ import 'moment/locale/ru' import moment from 'moment' -import { useState, useEffect, memo } from 'react' +import { useState, useEffect, memo, useCallback } from 'react' import { Radio, Button, Select, notification } from 'antd' import { ReportService } from '@api' @@ -43,7 +43,7 @@ export const Report = memo(({ idWell }) => { const [pagesCount, setPagesCount] = useState(0) const [showLoader, setShowLoader] = useState(false) - const handleReportCreation = async () => await invokeWebApiWrapperAsync( + const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync( async () => { const taskId = await ReportService.createReport( idWell, @@ -81,9 +81,11 @@ export const Report = memo(({ idWell }) => { ${filterDateRange[0].format(dateTimeFormat)} по ${filterDateRange[1].format(dateTimeFormat)}`, 'Создание отчёта по скважине' - ) + ), [filterDateRange, format, idWell, step]) - const disabledDate = (current) => !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]') + const disabledDate = useCallback((current) => + !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]') + , [aviableDateRange]) useEffect(() => invokeWebApiWrapperAsync( async () => { diff --git a/src/pages/TelemetryView/SetpointSender.jsx b/src/pages/TelemetryView/SetpointSender.jsx index 367191f..27bceae 100644 --- a/src/pages/TelemetryView/SetpointSender.jsx +++ b/src/pages/TelemetryView/SetpointSender.jsx @@ -6,7 +6,7 @@ import { Grid, GridItem } from '@components/Grid' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { makeNumericRender, EditableTable } from '@components/Table' -import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker' +import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' export const SetpointSender = ({ idWell, onClose, visible, setpointNames }) => { const [expirePeriod, setExpirePeriod] = useState(defaultPeriod) diff --git a/src/pages/TelemetryView/index.jsx b/src/pages/TelemetryView/index.jsx index b998289..3a9f28e 100644 --- a/src/pages/TelemetryView/index.jsx +++ b/src/pages/TelemetryView/index.jsx @@ -12,7 +12,7 @@ import { makeDateSorter } from '@components/Table' import LoaderPortal from '@components/LoaderPortal' import { Grid, GridItem, Flex } from '@components/Grid' import { invokeWebApiWrapperAsync } from '@components/factory' -import { PeriodPicker, defaultPeriod } from '@components/PeriodPicker' +import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { hasPermission } from '@utils/permissions' import { Subscribe } from '@services/signalr' diff --git a/src/pages/Well.jsx b/src/pages/Well.jsx index fe1e40b..5df9768 100644 --- a/src/pages/Well.jsx +++ b/src/pages/Well.jsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import { memo, useMemo } from 'react' import { FolderOutlined, FundViewOutlined, @@ -30,7 +30,7 @@ const { Content } = Layout export const Well = memo(() => { const { idWell, tab } = useParams() - const rootPath = `/well/${idWell}` + const rootPath = useMemo(() => `/well/${idWell}`, [idWell]) return ( diff --git a/src/pages/WellOperations/ImportExportBar.jsx b/src/pages/WellOperations/ImportExportBar.jsx index 34fe7d8..4e44ff7 100644 --- a/src/pages/WellOperations/ImportExportBar.jsx +++ b/src/pages/WellOperations/ImportExportBar.jsx @@ -12,8 +12,8 @@ const style = { margin: 4 } export const ImportExportBar = memo(({ idWell, onImported, disabled }) => { const [isImportModalVisible, setIsImportModalVisible] = useState(false) - const downloadTemplate = async () => download(`/api/well/${idWell}/wellOperations/template`) - const downloadExport = async () => download(`/api/well/${idWell}/wellOperations/export`) + const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`) + const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`) const onDone = () => { setIsImportModalVisible(false) diff --git a/src/pages/WellOperations/Tvd.jsx b/src/pages/WellOperations/Tvd.jsx deleted file mode 100644 index 0f6f5e4..0000000 --- a/src/pages/WellOperations/Tvd.jsx +++ /dev/null @@ -1,183 +0,0 @@ -import { Switch } from 'antd' -import { memo, useState, useRef, useEffect } from 'react' - -import { - Chart, - TimeScale, - LinearScale, - Legend, - LineController, - PointElement, - LineElement -} from 'chart.js' -import 'chartjs-adapter-moment' -import zoomPlugin from 'chartjs-plugin-zoom' -import ChartDataLabels from 'chartjs-plugin-datalabels' - -import LoaderPortal from '@components/LoaderPortal' -import { invokeWebApiWrapperAsync } from '@components/factory' -import { getOperations } from '@utils/functions' - -Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin) - -const scaleTypes = { - day: { - min: 0, - type: 'linear', - display: true, - title: { display: false, text: '' }, - ticks: { stepSize: 1 } - }, - date: { - display: true, - title: { display: true }, - type: 'time', - time: { - unit: 'hour', - displayFormats: { 'hour': 'MM.DD' } - }, - grid: { drawTicks: true }, - ticks: { - stepSize: 3, - major: { enabled: true }, - z: 1, - display: true, - textStrokeColor: '#fff', - textStrokeWidth: 2, - color: '#000', - } - } -} - -const defaultOptions = { - responsive: true, - aspectRatio: 2.6, - interaction: { - intersect: false, - mode: 'index', - }, - scales: { - x: scaleTypes.day, - y: { - type: 'linear', - position: 'top', - reverse: true, - display: true, - title: { - display: false, - text: '', - } - } - }, - parsing: { - xAxisKey: 'day', - yAxisKey: 'depth', - }, - elements: { - point: { - radius: 1.7, - //backgroundColor:'#aaa', - //pointStyle:'triangle', - }, - }, - plugins: { - legend: { - display: true, - }, - datalabels: { - display: false, - }, - tooltip: { - enabled: true, - callbacks: { - label: (tooltipItem) => tooltipItem.yLabel, - }, - position: 'nearest', - }, - } -} - -const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({ - label, - data, - backgroundColor: color, - borderColor: color, - borderWidth, - borderDash, -}) - -export const Tvd = memo(({ idWell, title }) => { - const [operations, setOperations] = useState([]) - const [isLoading, setIsLoading] = useState(false) - const [xLabel, setXLabel] = useState('day') - const [chart, setChart] = useState() - - const chartRef = useRef(null) - - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const operations = await getOperations(idWell) - setOperations(operations) - }, - setIsLoading, - `Не удалось загрузить операции по скважине "${idWell}"`, - 'Получение списка опервций по скважине' - ) - }, [idWell]) - - useEffect(() => { - const data = { - datasets: [ - makeDataset(operations?.fact, 'Факт', '#0A0'), - makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]), - makeDataset(operations?.plan, 'План', '#C004', 4), - ] - } - - if (chartRef.current && !chart) { - const thisOptions = {} - Object.assign(thisOptions, defaultOptions) - thisOptions.scales.x = scaleTypes[xLabel] - thisOptions.parsing.xAxisKey = xLabel - - const newChart = new Chart(chartRef.current, { - type: 'line', - plugins: [ChartDataLabels], - options: thisOptions, - data: data - }) - setChart(newChart) - - return () => chart?.destroy() - } else { - chart.data = data - chart.options.scales.x = scaleTypes[xLabel] - chart.options.parsing.xAxisKey = xLabel - chart.update() - } - }, [chart, operations, xLabel]) - - return ( -
-
- {title || ( -

График Глубина-день

- )} - - -
- setXLabel(checked ? 'date' : 'day')} - /> -
-
-
-
- ) -}) - -export default Tvd diff --git a/src/pages/WellOperations/Tvd/AdditionalTables.jsx b/src/pages/WellOperations/Tvd/AdditionalTables.jsx new file mode 100644 index 0000000..1889b63 --- /dev/null +++ b/src/pages/WellOperations/Tvd/AdditionalTables.jsx @@ -0,0 +1,67 @@ +import { Descriptions } from 'antd' +import { memo, useEffect, useState } from 'react' + +import { makeNumericRender } from '@components/Table' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { formatDate, fractionalSum } from '@utils/datetime' + +import '@styles/tvd.less' + +const { Item } = Descriptions + +const calcEndDate = (saubData) => { + if (!Array.isArray(saubData) || saubData.length <= 0) return [null, null] + const lastElm = saubData.at(-1) + return [saubData[0]?.date, fractionalSum(lastElm?.date, lastElm?.nptHours, 'hour')] +} + +const numericRender = makeNumericRender(2) +const printDate = (date) => formatDate(date) ?? '-' + +export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => { + const [additionalData, setAdditionalData] = useState({}) + + useEffect(() => invokeWebApiWrapperAsync( + async () => { + const [factStartDate, factEndDate] = calcEndDate(operations.fact) + const [planStartDate, planEndDate] = calcEndDate(operations.plan) + const [predictStartDate, predictEndDate] = calcEndDate(operations.predict) + + const last = predictEndDate ?? factEndDate + setAdditionalData({ + lag: (+new Date(last) - +new Date(planEndDate)) / 86400_000, + endDate: last, + factStartDate, + factEndDate, + planStartDate, + planEndDate, + predictStartDate, + predictEndDate, + }) + }, + setIsLoading, + 'Не удалось высчитать дополнительные данные' + ), [operations, setIsLoading]) + + return ( + <> +
+ + {printDate(additionalData.endDate)} + {numericRender(additionalData.lag)} + +
+
+ + {numericRender(additionalData.lag)} + {printDate(additionalData.planStartDate)} + {printDate(additionalData.factStartDate)} + {printDate(additionalData.planEndDate)} + {printDate(additionalData.factEndDate)} + +
+ + ) +}) + +export default AdditionalTables diff --git a/src/pages/WellOperations/Tvd/NetGraphExport.jsx b/src/pages/WellOperations/Tvd/NetGraphExport.jsx new file mode 100644 index 0000000..72a5d72 --- /dev/null +++ b/src/pages/WellOperations/Tvd/NetGraphExport.jsx @@ -0,0 +1,29 @@ +import { memo, useCallback, useState } from 'react' +import { ExportOutlined } from '@ant-design/icons' +import { Button } from 'antd' + +import { download, invokeWebApiWrapperAsync } from '@components/factory' + +export const NetGraphExport = memo(({ idWell, ...other }) => { + const [isFileExporting, setIsFileExporting] = useState(false) + + const onExport = useCallback(() => invokeWebApiWrapperAsync( + async () => await download(`/api/well/${idWell}/wellOperations/scheduleReport`), + setIsFileExporting, + 'Не удалось загрузить файл' + ), [idWell]) + + return ( + + ) +}) + +export default NetGraphExport diff --git a/src/pages/WellOperations/Tvd/NptTable.jsx b/src/pages/WellOperations/Tvd/NptTable.jsx new file mode 100644 index 0000000..12f39bf --- /dev/null +++ b/src/pages/WellOperations/Tvd/NptTable.jsx @@ -0,0 +1,68 @@ +import { memo, useEffect, useState } from 'react' +import { InputNumber } from 'antd' +import { FilterOutlined } from '@ant-design/icons' + +import LoaderPortal from '@components/LoaderPortal' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { makeDateColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' + +import '@styles/tvd.less' + +export const columns = [ + makeTextColumn('Конструкция секции', 'wellSectionTypeName', null, null, null, { width: 140 }), + makeNumericColumn('Глубина', 'depth', null, null, null, 80), + makeDateColumn('Дата начала', 'date', false, undefined, { width: 90 }), + makeNumericColumn('Длительность (ч)', 'durationHours', null, null, makeNumericRender(2), 120), + makeTextColumn('Доп. инфо', 'categoryInfo', null, null, null), + makeTextColumn('Комментарий', 'comment'), +] + +export const NptTable = memo(({ operations }) => { + const [filterValue, setFilterValue] = useState(0) + const [npt, setNPT] = useState([]) + const [filteredNPT, setFilteredNPT] = useState([]) + const [isTableLoading, setIsTableLoading] = useState(false) + + useEffect(() => invokeWebApiWrapperAsync( + async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []), + setIsTableLoading, + 'Не удалось получить список НПВ' + ), [operations]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)), + setIsTableLoading, + 'Не удалось отфильтровать НПВ по времени' + ), [npt, filterValue]) + + return ( +
+
+ + Фильтр время НПВ ≥ + setFilterValue(value ?? 0)} + value={filterValue} + /> + ч. +
+ +
+ + + ) +}) + +export default NptTable diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx new file mode 100644 index 0000000..e0e2784 --- /dev/null +++ b/src/pages/WellOperations/Tvd/index.jsx @@ -0,0 +1,226 @@ +import { useHistory } from 'react-router-dom' +import { memo, useState, useRef, useEffect, useCallback } from 'react' +import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' +import { Switch, Button } from 'antd' + +import { + Chart, + TimeScale, + LinearScale, + Legend, + LineController, + PointElement, + LineElement +} from 'chart.js' +import 'chartjs-adapter-moment' +import zoomPlugin from 'chartjs-plugin-zoom' +import ChartDataLabels from 'chartjs-plugin-datalabels' + +import LoaderPortal from '@components/LoaderPortal' +import { invokeWebApiWrapperAsync } from '@components/factory' +import { formatDate, fractionalSum } from '@utils/datetime' +import { getOperations } from '@utils/functions' + +import NptTable from './NptTable' +import NetGraphExport from './NetGraphExport' +import AdditionalTables from './AdditionalTables' + +import '@styles/index.css' +import '@styles/tvd.less' + +Chart.register( + TimeScale, + LinearScale, + LineController, + LineElement, + PointElement, + Legend, + ChartDataLabels, + zoomPlugin +) + +const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-' + +const scaleTypes = { + day: { + min: 0, + type: 'linear', + display: true, + title: { display: false, text: '' }, + ticks: { stepSize: 1 } + }, + date: { + display: true, + title: { display: true }, + type: 'time', + time: { unit: 'day', displayFormats: { day: 'DD.MM.YYYY' } }, + grid: { drawTicks: true }, + ticks: { + stepSize: 3, + major: { enabled: true }, + z: 1, + display: true, + textStrokeColor: '#fff', + textStrokeWidth: 2, + color: '#000', + } + } +} + +const defaultOptions = { + responsive: true, + maintainAspectRatio: false, + aspectRatio: false, + interaction: { intersect: false, mode: 'point' }, + scales: { + x: scaleTypes.day, + y: { + type: 'linear', + position: 'top', + reverse: true, + display: true, + title: { display: false, text: '' } + } + }, + parsing: { xAxisKey: 'day', yAxisKey: 'depth' }, + elements: { point: { radius: 1.7 } }, + plugins: { + legend: { display: true }, + datalabels: { display: false }, + tooltip: { + enabled: true, + position: 'nearest', + callbacks: { + title: (items) => [ + `Дата: ${formatDate(items[0].raw?.date) ?? '-'}`, + `День с начала бурения: ${parseInt(items[0].raw?.day)}`, + ], + afterTitle: (items) => `Глубина: ${numericRender(items[0].raw?.depth)}`, + label: (item) => [ + item.raw.wellSectionTypeName + ': ' + item.raw.categoryName, + `Длительность (ч): ${item.raw.nptHours}` + ], + }, + }, + }, +} + +const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({ + label, + data, + backgroundColor: color, + borderColor: color, + borderWidth, + borderDash, +}) + +export const Tvd = memo(({ idWell, title }) => { + const [chart, setChart] = useState() + const [xLabel, setXLabel] = useState('day') + const [operations, setOperations] = useState({}) + const [tableVisible, setTableVisible] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + const chartRef = useRef(null) + const history = useHistory() + + const onPointClick = useCallback((e) => { + const points = e?.chart?.tooltip?.dataPoints + if (!points || !(points.length > 0)) return + + const datasetId = points.find((p) => p.datasetIndex !== 1)?.datasetIndex + if (typeof datasetId === 'undefined') return + + const datasetName = datasetId === 2 ? 'plan' : 'fact' + const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',') + history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`) + }, [idWell, history]) + + useEffect(() => invokeWebApiWrapperAsync( + async () => setOperations(await getOperations(idWell)), + setIsLoading, + `Не удалось загрузить операции по скважине "${idWell}"`, + 'Получение списка опервций по скважине' + ), [idWell]) + + useEffect(() => { + const withoutNpt = [] + operations?.fact?.forEach((row) => { + if (row?.isNPT !== false) return + const nptH = +(row.nptHours ?? 0) + withoutNpt.push({ + ...row, + day: row.day - nptH / 24, + date: fractionalSum(row.date, -nptH, 'hour'), + }) + }) + + const data = { datasets: [ + makeDataset(operations?.fact, 'Факт', '#0A0', 3), + makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]), + makeDataset(operations?.plan, 'План', '#F00', 3), + makeDataset(withoutNpt, 'Факт без НПВ', '#00F', 3) + ]} + + if (chartRef.current && !chart) { + const thisOptions = {} + Object.assign(thisOptions, defaultOptions) + thisOptions.onClick = onPointClick + thisOptions.scales.x = scaleTypes[xLabel] + thisOptions.parsing.xAxisKey = xLabel + + const newChart = new Chart(chartRef.current, { + type: 'line', + plugins: [ChartDataLabels], + options: thisOptions, + data: data + }) + setChart(newChart) + + return () => chart?.destroy() + } else { + chart.data = data + chart.options.scales.x = scaleTypes[xLabel] + chart.options.parsing.xAxisKey = xLabel + chart.update() + // Обнуление ширины необходимо для уменьшения размена при resize после появления элементов + chart.canvas.parentNode.style.width = '0' + chart.resize() + } + }, [chart, operations, xLabel, onPointClick]) + + const toogleTable = useCallback(() => { + setOperations(pre => ({ ...pre })) + setTableVisible(v => !v) + }, []) + + return ( +
+
+

{title || 'График Глубина-день'}

+
+ setXLabel(checked ? 'date' : 'day')} + style={{ marginRight: '20px' }} + /> + + +
+
+ +
+
+ + +
+ {tableVisible && } +
+
+
+ ) +}) + +export default Tvd diff --git a/src/pages/WellOperations/WellDrillParams.jsx b/src/pages/WellOperations/WellDrillParams.jsx index 87c8407..8db51b6 100644 --- a/src/pages/WellOperations/WellDrillParams.jsx +++ b/src/pages/WellOperations/WellDrillParams.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, memo } from 'react' +import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { EditableTable, @@ -54,15 +54,15 @@ export const WellDrillParams = memo(({ idWell }) => { await updateParams() })(), [idWell, updateParams]) - const handlerProps = { + const handlerProps = useMemo(() => ({ service: DrillParamsService, setLoader: setShowLoader, errorMsg: `Не удалось выполнить операцию`, onComplete: updateParams, idWell - } + }), [idWell, updateParams]) - const recordParser = (record) => ({ ...record, idWell }) + const recordParser = useCallback((record) => ({ ...record, idWell }), [idWell]) return ( diff --git a/src/pages/WellOperations/WellOperationsEditor.jsx b/src/pages/WellOperations/WellOperationsEditor.jsx index 1266d49..033a6f0 100644 --- a/src/pages/WellOperations/WellOperationsEditor.jsx +++ b/src/pages/WellOperations/WellOperationsEditor.jsx @@ -1,86 +1,88 @@ import moment from 'moment' import { Input } from 'antd' -import { useState, useEffect, memo } from 'react' +import { useLocation } from 'react-router-dom' +import { useState, useEffect, memo, useMemo, useCallback } from 'react' import { EditableTable, - DatePickerWrapper, makeColumn, - makeDateSorter, makeNumericColumnOptions, makeSelectColumn, makeActionHandler, + makeDateColumn, + makeNumericColumn, + makeNumericRender, + makeNumericSorter, + makeTextColumn, } from '@components/Table' -import { WellOperationService} from '@api' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { hasPermission } from '@utils/permissions' -import { formatDate } from '@utils' +import { arrayOrDefault } from '@utils' +import { WellOperationService } from '@api' const { TextArea } = Input const basePageSize = 160 +const dayRender = makeNumericRender(2) +const dayWithoutNptRender = (_, row) => dayRender((row.day ?? 0) - (row.nptHours ?? 0) / 24) -const defaultColumns = [ - makeSelectColumn('Конструкция секции', 'idWellSectionType', [], undefined, { +const generateColumns = (showNpt = false, categories = [], sectionTypes = []) => [ + makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, undefined, { + sorter: makeNumericSorter('idWellSectionType'), editable: true, width: 160, - formItemRules: [({ getFieldValue }) => ({ - validator(_, value) { - if (value?.length > 0) - return Promise.resolve() - return Promise.reject('Это обязательное поле!') - } - })], }), - makeSelectColumn('Операция', 'idCategory', [], undefined, { editable: true, width: 200 }), - makeColumn('Доп. инфо', 'categoryInfo', { editable: true, width: 300, input: