forked from ddrilling/asb_cloud_front
Программа бурения разделена на директорию и завершена
This commit is contained in:
parent
7a33c5ef2f
commit
2c44899212
@ -1,236 +0,0 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { Button, Tooltip, Layout } from 'antd'
|
||||
import { CommentOutlined, FileWordOutlined, TableOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { Flex } from '@components/Grid'
|
||||
import { UserView } from '@components/views'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { arrayOrDefault, formatDate } from '@utils'
|
||||
import { hasPermission } from '@utils/permissions'
|
||||
import { DrillingProgramService } from '@api'
|
||||
|
||||
const testCategories = [
|
||||
{
|
||||
caption: 'Задание от геологов',
|
||||
status: 1,
|
||||
file: {
|
||||
name: 'Документ 1.xlsx',
|
||||
author: { login: 'Человек 9', company: { caption: 'Компания 9' } },
|
||||
size: '123 кБ',
|
||||
uploadDate: '2022-01-01T00:00:00',
|
||||
},
|
||||
lastApprove: '2022-01-01T00:00:00',
|
||||
lastReject: '2022-01-01T00:00:00',
|
||||
approved: [{
|
||||
user: { login: 'Человек 2', company: { caption: 'Компания 2' }},
|
||||
comment: 'Комментарий',
|
||||
}],
|
||||
undecided: [{ login: 'Человек 3', company: { caption: 'Компания 1' }}],
|
||||
rejected: [],
|
||||
}, {
|
||||
caption: 'Профиль ствола скважины (ННБ)',
|
||||
status: 1,
|
||||
file: {
|
||||
name: 'Документ 1.xlsx',
|
||||
author: { login: 'Человек 9', company: { caption: 'Компания 9' }},
|
||||
size: '123 кБ',
|
||||
uploadDate: '2022-01-01T00:00:00',
|
||||
},
|
||||
lastApprove: '2022-01-01T00:00:00',
|
||||
lastReject: '2022-01-01T00:00:00',
|
||||
approved: [{
|
||||
user: { login: 'Человек 2', company: { caption: 'Компания 2' }},
|
||||
comment: 'Комментарий 3',
|
||||
}],
|
||||
undecided: [
|
||||
{ login: 'Человек 3', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 4', company: { caption: 'Компания 2' }},
|
||||
],
|
||||
rejected: [{
|
||||
user: { login: 'Человек 1', company: { caption: 'Компания 1' }},
|
||||
comment: 'Комментарий 2',
|
||||
}],
|
||||
}, {
|
||||
caption: 'Технологические расчёты ННБ',
|
||||
status: 0,
|
||||
file: {
|
||||
name: 'Документ 1.xlsx',
|
||||
author: { login: 'Человек 9', company: { caption: 'Компания 9' }},
|
||||
size: '123 кБ',
|
||||
uploadDate: '2022-01-01T00:00:00',
|
||||
},
|
||||
lastApprove: '2022-01-01T00:00:00',
|
||||
lastReject: '2022-01-01T00:00:00',
|
||||
approved: [],
|
||||
undecided: [
|
||||
{ login: 'Человек 2', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 3', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 4', company: { caption: 'Компания 2' }},
|
||||
],
|
||||
rejected: [{
|
||||
user: { login: 'Человек 1', company: { caption: 'Компания 1' }},
|
||||
comment: 'Комментарий 2',
|
||||
}],
|
||||
}, {
|
||||
caption: 'ГГД',
|
||||
status: 0,
|
||||
file: {
|
||||
name: 'Документ 1.xlsx',
|
||||
author: { login: 'Человек 9', company: { caption: 'Компания 9' }},
|
||||
size: '123 кБ',
|
||||
uploadDate: '2022-01-01T00:00:00',
|
||||
},
|
||||
lastApprove: '2022-01-01T00:00:00',
|
||||
lastReject: '2022-01-01T00:00:00',
|
||||
approved: [],
|
||||
undecided: [
|
||||
{ login: 'Человек 2', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 3', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 4', company: { caption: 'Компания 2' }},
|
||||
{ login: 'Человек 2', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 3', company: { caption: 'Компания 1' }},
|
||||
{ login: 'Человек 4', company: { caption: 'Компания 2' }},
|
||||
],
|
||||
rejected: [{
|
||||
user: { login: 'Человек 1', company: { caption: 'Компания 1' }},
|
||||
comment: 'Комментарий 2',
|
||||
}],
|
||||
}
|
||||
]
|
||||
|
||||
const testProgram = {
|
||||
name: 'Документ 1.xlsx',
|
||||
size: '123 кБ',
|
||||
uploadDate: '2022-01-01T00:00:00',
|
||||
}
|
||||
|
||||
export const DrillingProgram = memo(({ idWell }) => {
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [categories, setCategories] = useState(testCategories)
|
||||
const [program, setProgram] = useState(testProgram)
|
||||
|
||||
const updateData = async () => await invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const categories = arrayOrDefault(await DrillingProgramService.getCategories(idWell))
|
||||
const program = await DrillingProgramService.getProgram(idWell)
|
||||
setCategories(categories)
|
||||
setProgram(program)
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось загрузить название скважины "${idWell}"`
|
||||
)
|
||||
|
||||
// useEffect(() => updateCategories(), [idWell])
|
||||
|
||||
const onApprove = (category) => () => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await DrillingProgramService.approve(idWell, category)
|
||||
await updateData()
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось согласовать документ для скважины "${idWell}"!`
|
||||
)
|
||||
|
||||
const onReject = (category) => () => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await DrillingProgramService.reject(idWell, category)
|
||||
await updateData()
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось согласовать документ для скважины "${idWell}"!`
|
||||
)
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Layout>
|
||||
{program && (
|
||||
<div style={{ border: '1px solid #BEBEBE' }}>
|
||||
<div style={{ padding: '5px 10px', backgroundColor: '#F3F3F3', borderBottom: '1px solid #BEBEBE' }}>Программа бурения</div>
|
||||
<Flex style={{ justifyContent: 'flex-start', alignItems: 'center', backgroundColor: 'white' }}>
|
||||
<Button type={'link'} icon={<FileWordOutlined />} style={{ marginLeft: '10px' }}>{program.name}</Button>
|
||||
<div style={{ margin: '10px' }}>Размер: {program.size}</div>
|
||||
<div style={{ margin: '10px' }}>Загружен: {formatDate(program.uploadDate)}</div>
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categories.map((category) => category && (
|
||||
<div key={category.caption} style={{ border: '1px solid #BEBEBE', marginTop: '10px' }}>
|
||||
<Flex style={{ width: '100%', borderBottom: '1px solid #BEBEBE' }}>
|
||||
<div style={{ padding: '5px 10px', backgroundColor: '#F3F3F3', flex: 5, borderRight: '1px solid #BEBEBE' }}>{category.caption}</div>
|
||||
<div style={{ padding: '5px 10px', backgroundColor: '#F3F3F3', flex: 7 }}>Согласованты</div>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex style={{ padding: '5px 10px', backgroundColor: 'white', alignItems: 'stretch', flex: 5, borderRight: '1px solid #BEBEBE' }}>
|
||||
<Flex style={{ flex: 10, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Button type={'link'} icon={<FileWordOutlined />}>{category.file.name}</Button>
|
||||
<div style={{ marginLeft: '15px' }}>Автор: <UserView user={category.file.author}/></div>
|
||||
<div style={{ marginLeft: '15px' }}>Размер: {category.file.size}</div>
|
||||
<div style={{ marginLeft: '15px' }}>Загружен: {formatDate(category.file.uploadDate)}</div>
|
||||
</Flex>
|
||||
<Flex style={{ flexDirection: 'column', flex: 2 }}>
|
||||
<Button icon={<UploadOutlined />} style={{ margin: '5px 0 10px 0' }} title={'Загрузить'}>Загрузить</Button>
|
||||
<Button icon={<TableOutlined />} disabled={!category.file} title={'История'}>История</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex style={{ padding: '5px 10px', backgroundColor: 'white', alignItems: 'stretch', flex: 7 }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', flex: 6 }}>
|
||||
{category.undecided.map((user, i) => (
|
||||
<span key={i} style={{ margin: '5px 10px' }}>
|
||||
<UserView user={user} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Flex style={{ flex: 3, margin: '0 5px', flexDirection: 'column' }}>
|
||||
{category.status === 0 && hasPermission() && (
|
||||
<Button style={{ margin: '5px 0'}} onClick={onApprove(category)}>Согласовать</Button>
|
||||
)}
|
||||
<Flex style={{ flexDirection: 'column', padding: '10px', backgroundColor: '#EFFEEF', border: '1px solid #1FB448', flexGrow: 1 }}>
|
||||
<Flex style={{ justifyContent: 'space-between' }}>
|
||||
<span style={{ fontWeight: 800 }}>Согласовано</span>
|
||||
<span>{formatDate(category.lastApprove)}</span>
|
||||
</Flex>
|
||||
<div style={{ display: 'flex', flexDirection: 'column-reverse', alignItems: 'stretch', flexGrow: 1 }}>
|
||||
{category.approved?.map(({ comment, user }, i) => (
|
||||
<div key={`${i}`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<UserView user={user} />
|
||||
<Tooltip placement={'left'} title={comment}>
|
||||
<CommentOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex style={{ flex: 3, marginLeft: '0 5px', flexDirection: 'column' }}>
|
||||
{category.status === 0 && hasPermission() && (
|
||||
<Button style={{ margin: '5px 0' }} onClick={onReject(category)}>Отклонить</Button>
|
||||
)}
|
||||
<Flex style={{ flexDirection: 'column', padding: '10px', backgroundColor: '#FEF2EF', border: '1px solid #B4661F', flexGrow: 1 }}>
|
||||
<Flex style={{ justifyContent: 'space-between' }}>
|
||||
<span style={{ fontWeight: 800 }}>Отклонено</span>
|
||||
<span>{formatDate(category.lastReject)}</span>
|
||||
</Flex>
|
||||
<div style={{ display: 'flex', flexDirection: 'column-reverse', alignItems: 'stretch', flexGrow: 1 }}>
|
||||
{category.rejected?.map(({ comment, user }, i) => (
|
||||
<div key={`${i}`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<UserView user={user} />
|
||||
<Tooltip placement={'left'} title={comment}>
|
||||
<CommentOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</Layout>
|
||||
</LoaderPortal>
|
||||
)
|
||||
})
|
||||
|
||||
export default DrillingProgram
|
61
src/pages/DrillingProgram/CategoryAdder.jsx
Normal file
61
src/pages/DrillingProgram/CategoryAdder.jsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Button, Select } from 'antd'
|
||||
import { FileAddOutlined } from '@ant-design/icons'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { DrillingProgramService } from '@api'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
|
||||
import '@styles/drilling_program.less'
|
||||
|
||||
export const CategoryAdder = memo(({ categories, idWell, onUpdate, className, ...other }) => {
|
||||
const [options, setOptions] = useState([])
|
||||
const [value, setValue] = useState([])
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [showCatLoader, setShowCatLoader] = useState(false)
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
setOptions(categories.map((category) => ({
|
||||
label: category.name ?? category.shortName,
|
||||
value: category.id
|
||||
})))
|
||||
},
|
||||
setShowCatLoader,
|
||||
`Не удалось установить список доступных категорий для добавления`
|
||||
), [categories])
|
||||
|
||||
const onAddClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await DrillingProgramService.addParts(idWell, value)
|
||||
setValue([])
|
||||
onUpdate?.()
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось добавить новые категорий программы бурения`,
|
||||
`Добавление категорий программы бурения`
|
||||
), [onUpdate, idWell, value])
|
||||
|
||||
return (
|
||||
<div className={`category_adder ${className ?? ''}`} {...other}>
|
||||
<Select
|
||||
allowClear
|
||||
className={'adder_select'}
|
||||
mode={'multiple'}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
loading={showCatLoader}
|
||||
/>
|
||||
<Button
|
||||
onClick={onAddClick}
|
||||
loading={showLoader}
|
||||
icon={<FileAddOutlined />}
|
||||
disabled={!value || value.length <= 0}
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default CategoryAdder
|
162
src/pages/DrillingProgram/CategoryEditor.jsx
Normal file
162
src/pages/DrillingProgram/CategoryEditor.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { Input, Modal, Radio } from 'antd'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { UserView } from '@components/views'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { makeColumn, makeNumericSorter, Table } from '@components/Table'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DrillingProgramService } from '@api'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
|
||||
const userRules = [
|
||||
{ label: 'Нет', value: 0 },
|
||||
{ label: 'Публикатор', value: 1 },
|
||||
{ label: 'Согласовант', value: 2 },
|
||||
]
|
||||
|
||||
const SEARCH_TIMEOUT = 1000
|
||||
|
||||
export const CategoryEditor = memo(({ idWell, visible, category, onClosed }) => {
|
||||
const [title, setTitle] = useState()
|
||||
const [users, setUsers] = useState([])
|
||||
const [allUsers, setAllUsers] = useState([])
|
||||
const [filteredUsers, setFilteredUsers] = useState([])
|
||||
const [searchTimeout, setSearchTimeout] = useState(null)
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell))
|
||||
setAllUsers(allUsers)
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось загрузить список доступных пользователей скважины "${idWell}"`
|
||||
), [idWell])
|
||||
|
||||
const calcFilteredUsers = useCallback(async (value, users) => await invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
if (searchTimeout)
|
||||
clearTimeout(searchTimeout)
|
||||
setSearchTimeout(null)
|
||||
const filteredUsers = users.filter(({ user }) => user && [
|
||||
user.login ?? '',
|
||||
user.name ?? '',
|
||||
user.surname ?? '',
|
||||
user.partonymic ?? '',
|
||||
user.email ?? '',
|
||||
user.phone ?? '',
|
||||
user.position ?? '',
|
||||
user.company?.caption ?? '',
|
||||
].join(' ').toLowerCase().includes(value))
|
||||
setFilteredUsers(filteredUsers)
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось произвести поиск пользователей`
|
||||
), [searchTimeout])
|
||||
|
||||
const calcUsers = useCallback(() => {
|
||||
if (!visible) return
|
||||
setTitle(category?.name ? `"${category.name}"` : '')
|
||||
|
||||
const approvers = arrayOrDefault(category?.approvers)
|
||||
const publishers = arrayOrDefault(category?.publishers)
|
||||
const otherUsers = allUsers.filter((user) => [...approvers, ...publishers].findIndex(u => u.id === user.id) < 0)
|
||||
const users = [
|
||||
...otherUsers.map((user) => ({ user, status: 0 })),
|
||||
...publishers.map((user) => ({ user, status: 1 })),
|
||||
...approvers.map((user) => ({ user, status: 2 })),
|
||||
]
|
||||
setUsers(users)
|
||||
calcFilteredUsers(searchValue, users)
|
||||
}, [category, visible, allUsers, calcFilteredUsers, searchValue])
|
||||
|
||||
useEffect(calcUsers, [calcUsers])
|
||||
|
||||
const changeUserStatus = useCallback((user, status) => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const userIdx = users?.findIndex(({ user: u }) => u.id === user.id)
|
||||
if (!userIdx) return
|
||||
|
||||
if (status === 0) {
|
||||
await DrillingProgramService.removeUser(idWell, user.id, category.idFileCategory, users[userIdx].status)
|
||||
} else {
|
||||
await DrillingProgramService.addUser(idWell, user.id, category.idFileCategory, status)
|
||||
}
|
||||
setUsers((prevUsers) => {
|
||||
prevUsers[userIdx].status = status
|
||||
return prevUsers
|
||||
})
|
||||
},
|
||||
setShowLoader,
|
||||
<>
|
||||
Не удалось изменить статус пользователя
|
||||
<UserView user={user} />
|
||||
</>,
|
||||
`Изменение статуса пользователя`
|
||||
), [users, idWell, category.idFileCategory])
|
||||
|
||||
const userColumns = [
|
||||
makeColumn('Пользователь', 'user', {
|
||||
sorter: (a, b) => (a?.user?.surname && b?.user?.surname) ? a.user.surname.localeCompare(b.user.surname) : 0,
|
||||
render: (user) => <UserView user={user} />,
|
||||
}),
|
||||
makeColumn('Статус', 'status', {
|
||||
sorter: makeNumericSorter('status'),
|
||||
defaultSortOrder: 'descend',
|
||||
render: (status, { user }) => (
|
||||
<Radio.Group
|
||||
options={userRules}
|
||||
value={status ?? 0}
|
||||
onChange={(e) => changeUserStatus(user, e?.target?.value)}
|
||||
/>
|
||||
),
|
||||
})
|
||||
]
|
||||
|
||||
const onSearchTextChange = useCallback((value) => {
|
||||
value = value.trim().toLowerCase() ?? ''
|
||||
setSearchValue(value)
|
||||
|
||||
if (!searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
setSearchTimeout(setTimeout(() => calcFilteredUsers(value, users), SEARCH_TIMEOUT))
|
||||
}
|
||||
}, [searchTimeout, calcFilteredUsers, users])
|
||||
|
||||
const onModalCanceled = useCallback(() => {
|
||||
onClosed?.()
|
||||
calcUsers()
|
||||
}, [onClosed, calcUsers])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
width={1000}
|
||||
visible={visible}
|
||||
footer={null}
|
||||
onCancel={onModalCanceled}
|
||||
title={`Редактирование категории ${title}`}
|
||||
>
|
||||
<div>
|
||||
<Input.Search
|
||||
allowClear
|
||||
placeholder={'Поиск пользователя'}
|
||||
onChange={(e) => onSearchTextChange(e?.target?.value)}
|
||||
onSearch={(text) => calcFilteredUsers(text, users)}
|
||||
value={searchValue}
|
||||
style={{ marginBottom: '15px' }}
|
||||
/>
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Table
|
||||
bordered
|
||||
columns={userColumns}
|
||||
dataSource={filteredUsers}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
||||
export default CategoryEditor
|
127
src/pages/DrillingProgram/CategoryHistory.jsx
Normal file
127
src/pages/DrillingProgram/CategoryHistory.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Button, DatePicker, Input, Modal } from 'antd'
|
||||
|
||||
import { CompanyView } from '@components/views'
|
||||
import DownloadLink from '@components/DownloadLink'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
|
||||
import { arrayOrDefault, formatDate } from '@utils'
|
||||
import { FileService } from '@api'
|
||||
|
||||
import MarksCard from './MarksCard'
|
||||
|
||||
import '@styles/drilling_program.less'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
const { Search } = Input
|
||||
|
||||
export const historyColumns = [
|
||||
makeColumn('Имя файла', 'name', {
|
||||
sorter: makeStringSorter('name'),
|
||||
render: (name, file) => <DownloadLink file={file} name={name} />
|
||||
}),
|
||||
makeColumn('Компания', 'author', {
|
||||
sorter: (a, b) => makeStringSorter('caption')(a?.author?.company, b?.author?.company),
|
||||
render: (author) => <CompanyView company={author?.company}/>
|
||||
}),
|
||||
makeColumn('Дата публикации', 'uploadDate', {
|
||||
sorter: makeDateSorter('uploadDate'),
|
||||
render: (date) => formatDate(date),
|
||||
}),
|
||||
makeColumn('Отметки', 'fileMarks', {
|
||||
render: (marks) => {
|
||||
const approved = [], rejected = []
|
||||
arrayOrDefault(marks).forEach((mark) => {
|
||||
if (mark.idMarkType === 0) rejected.push(mark)
|
||||
if (mark.idMarkType === 1) approved.push(mark)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={'history_approve_column'} >
|
||||
<div className={'approve_list'}>
|
||||
<MarksCard title={'Согласовано'} className={'approve_panel'} marks={approved} />
|
||||
</div>
|
||||
<div className={'reject_list'}>
|
||||
<MarksCard title={'Отклонено'} className={'reject_panel'} marks={rejected} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
export const CategoryHistory = ({ idWell, idCategory, visible, onClose }) => {
|
||||
const [data, setData] = useState([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(14)
|
||||
const [range, setRange] = useState([])
|
||||
const [fileName, setFileName] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [companyName, setCompanyName] = useState('')
|
||||
|
||||
useEffect(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
if (!visible) return
|
||||
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
const paginatedHistory = await FileService.getFilesInfo(idWell, idCategory, companyName, fileName, begin, end, skip, pageSize)
|
||||
setTotal(paginatedHistory?.count ?? 0)
|
||||
setData(arrayOrDefault(paginatedHistory?.items))
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"`
|
||||
), [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
|
||||
|
||||
const onPaginationChange = useCallback((page, pageSize) => {
|
||||
setPage(page)
|
||||
setPageSize(pageSize)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={'История категории'}
|
||||
width={1200}
|
||||
centered
|
||||
visible={!!visible}
|
||||
onCancel={onClose}
|
||||
footer={(
|
||||
<Button onClick={onClose}>Закрыть</Button>
|
||||
)}
|
||||
>
|
||||
<LoaderPortal show={isLoading}>
|
||||
<div className={'filter-group'}>
|
||||
<div className={'filter-group-heading'}>
|
||||
<Search
|
||||
className={'filter-selector'}
|
||||
placeholder={'Фильтр по имени файла'}
|
||||
onSearch={(value) => setFileName(value)}
|
||||
/>
|
||||
<Search
|
||||
className={'filter-selector'}
|
||||
placeholder={'Фильтр по названии компании'}
|
||||
onSearch={(value) => setCompanyName(value)}
|
||||
/>
|
||||
<RangePicker showTime onChange={(range) => setRange(range)} />
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
size={'small'}
|
||||
columns={historyColumns}
|
||||
dataSource={data}
|
||||
pagination={{
|
||||
total: total,
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
showSizeChanger: true,
|
||||
onChange: onPaginationChange
|
||||
}}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CategoryHistory
|
158
src/pages/DrillingProgram/CategoryRender.jsx
Normal file
158
src/pages/DrillingProgram/CategoryRender.jsx
Normal file
@ -0,0 +1,158 @@
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Button, Input, Popconfirm, Form } from 'antd'
|
||||
import {
|
||||
ClearOutlined,
|
||||
SettingOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
import { formatDate } from '@utils'
|
||||
import { DrillingProgramService } from '@api'
|
||||
import { formatBytes, invokeWebApiWrapperAsync, notify } from '@components/factory'
|
||||
import { UploadForm } from '@components/UploadForm'
|
||||
import DownloadLink from '@components/DownloadLink'
|
||||
import { UserView } from '@components/views'
|
||||
|
||||
import MarksCard from './MarksCard'
|
||||
|
||||
import '@styles/drilling_program.less'
|
||||
import Poprompt from '@asb/components/Poprompt'
|
||||
|
||||
const CommentPrompt = memo((props) => (
|
||||
<Poprompt
|
||||
buttonProps={{ className: 'mv-5' }}
|
||||
{...props}
|
||||
>
|
||||
<Form.Item
|
||||
label={'Комментарий'}
|
||||
name={'comment'}
|
||||
rules={[{ required: true, message: 'Пожалуйста, введите комментарий!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Poprompt>
|
||||
))
|
||||
|
||||
export const CategoryRender = memo(({ idWell, partData, onUpdate, onEdit, onHistory, setIsLoading, ...other }) => {
|
||||
const {
|
||||
idFileCategory,
|
||||
name: title, // Название категории
|
||||
idState, // Состояние категории
|
||||
approvers, // Полный список согласовантов
|
||||
permissionToApprove, // Наличие прав на согласование/отклонение документа
|
||||
permissionToUpload, // Наличие прав на загрузку нового файла
|
||||
file // Информация о файле
|
||||
} = partData ?? {}
|
||||
|
||||
const uploadUrl = `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
const onApprove = useCallback((approve = true) => (values) => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
if (!file?.id || !permissionToApprove || !values?.comment) return
|
||||
await DrillingProgramService.addOrReplaceFileMark(idWell, {
|
||||
idFile: file.id,
|
||||
idMarkType: approve ? 1 : 0,
|
||||
comment: values.comment
|
||||
})
|
||||
await onUpdate?.()
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось ${approve ? 'согласовать' : 'отклонить'} документ для скважины "${idWell}"!`,
|
||||
`${approve ? 'Согласование' : 'Отклонение'} документа "${title}" скважины "${idWell}"`
|
||||
), [idWell, setIsLoading, file, permissionToApprove, title, onUpdate])
|
||||
|
||||
const onRemoveClick = useCallback(() => invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await DrillingProgramService.removeParts(idWell, [idFileCategory])
|
||||
onUpdate?.()
|
||||
},
|
||||
setIsDeleting,
|
||||
`Не удалось удалить категорию "${title}" для скважины "${idWell}"`,
|
||||
`Удаление категории "${title}" скважины "${idWell}"`
|
||||
), [idWell, idFileCategory, onUpdate, title])
|
||||
|
||||
return (
|
||||
<div className={'drilling_category'} {...other}>
|
||||
<div className={'category_header'}>
|
||||
<h3>{title}</h3>
|
||||
<div>
|
||||
<Button icon={<SettingOutlined />} onClick={() => onEdit?.(idFileCategory)}>Редактировать</Button>
|
||||
<Popconfirm
|
||||
onConfirm={onRemoveClick}
|
||||
title={<>Вы уверены, что хотите удалить категорию<br/>"{title}"?</>}
|
||||
>
|
||||
<Button loading={isDeleting} icon={<ClearOutlined />}>Удалить</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'category_content'}>
|
||||
<div className={'file_column'}>
|
||||
<div className={'file_info'}>
|
||||
{file ? (
|
||||
<>
|
||||
<DownloadLink file={file} />
|
||||
<div className={'ml-15'}>Автор: <UserView user={file.author ?? '-'}/></div>
|
||||
<div className={'ml-15'}>Размер: {file.size ? formatBytes(file.size) : '-'}</div>
|
||||
<div className={'ml-15'}>Загружен: {formatDate(file.uploadDate) ?? '-'}</div>
|
||||
</>
|
||||
) : (
|
||||
<div>Нет загруженных файлов</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={'file_actions'}>
|
||||
<UploadForm
|
||||
url={uploadUrl}
|
||||
disabled={!permissionToUpload}
|
||||
style={{ margin: '5px 0 10px 0' }}
|
||||
onUploadStart={() => setIsUploading(true)}
|
||||
onUploadComplete={() => onUpdate?.(idFileCategory)}
|
||||
onUploadError={(e) => notify(e?.message ?? 'Ошибка загрузки файла', 'error')}
|
||||
/>
|
||||
<Button
|
||||
disabled={!file}
|
||||
title={'История'}
|
||||
icon={<TableOutlined />}
|
||||
onClick={() => onHistory?.(idFileCategory)}
|
||||
>
|
||||
История
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'approve_column'}>
|
||||
<div className={'user_list'}>
|
||||
{approvers.map((user, i) => (
|
||||
<span key={i}>
|
||||
<UserView user={user} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className={'approve_list'}>
|
||||
{permissionToApprove && (
|
||||
<CommentPrompt
|
||||
text={'Согласовать'}
|
||||
title={'Согласование документа'}
|
||||
onDone={onApprove(true)}
|
||||
/>
|
||||
)}
|
||||
<MarksCard title={'Согласовано'} className={'approve_panel'} marks={file?.fileMarks?.filter((mark) => mark.idMarkType === 1)} />
|
||||
</div>
|
||||
<div className={'reject_list'}>
|
||||
{permissionToApprove && (
|
||||
<CommentPrompt
|
||||
text={'Отклонить'}
|
||||
title={'Отклонение документа'}
|
||||
onDone={onApprove(false)}
|
||||
/>
|
||||
)}
|
||||
<MarksCard title={'Отклонено'} className={'reject_panel'} marks={file?.fileMarks?.filter((mark) => mark.idMarkType === 0)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default CategoryRender
|
25
src/pages/DrillingProgram/MarksCard.jsx
Normal file
25
src/pages/DrillingProgram/MarksCard.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { memo } from 'react'
|
||||
import { Tooltip } from 'antd'
|
||||
import { CommentOutlined } from '@ant-design/icons'
|
||||
|
||||
import { UserView } from '@components/views'
|
||||
import { formatDate } from '@utils'
|
||||
|
||||
export const MarksCard = memo(({ title, marks, className, ...other }) => (
|
||||
<div className={`panel ${className ?? ''}`} {...other}>
|
||||
<span className={'panel_title'}>{title}</span>
|
||||
<div className={'panel_content'}>
|
||||
{marks?.map(({ dateCreated, comment, user }, i) => (
|
||||
<div key={`${i}`}>
|
||||
<UserView user={user} />
|
||||
<span>{formatDate(dateCreated)}</span>
|
||||
<Tooltip title={comment}>
|
||||
<CommentOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
export default MarksCard
|
108
src/pages/DrillingProgram/index.jsx
Normal file
108
src/pages/DrillingProgram/index.jsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { Button, Layout } from 'antd'
|
||||
import { FileWordOutlined } from '@ant-design/icons'
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { Flex } from '@components/Grid'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { arrayOrDefault, formatDate } from '@utils'
|
||||
import { DrillingProgramService } from '@api'
|
||||
|
||||
import CategoryAdder from './CategoryAdder'
|
||||
import CategoryRender from './CategoryRender'
|
||||
import CategoryEditor from './CategoryEditor'
|
||||
import CategoryHistory from './CategoryHistory'
|
||||
|
||||
import '@styles/drilling_program.less'
|
||||
|
||||
export const DrillingProgram = memo(({ idWell }) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState()
|
||||
const [historyVisible, setHistoryVisible] = useState(false)
|
||||
const [editorVisible, setEditorVisible] = useState(false)
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [data, setData] = useState({})
|
||||
|
||||
const updateData = useCallback(async () => await invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const data = await DrillingProgramService.getState(idWell)
|
||||
const categories = arrayOrDefault(await DrillingProgramService.getCategories(idWell))
|
||||
setData(data)
|
||||
setCategories(categories.filter(cat => {
|
||||
if (cat?.id && (cat.name || cat.shortName))
|
||||
if (data?.parts.findIndex((val) => val.idFileCategory === cat.id) < 0)
|
||||
return true
|
||||
return false
|
||||
}))
|
||||
},
|
||||
setShowLoader,
|
||||
`Не удалось загрузить название скважины "${idWell}"`
|
||||
), [idWell])
|
||||
|
||||
useEffect(() => updateData(), [updateData])
|
||||
|
||||
const onCategoryEdit = (catId) => {
|
||||
setSelectedCategory(catId)
|
||||
setEditorVisible(!!catId)
|
||||
}
|
||||
|
||||
const onCategoryHistory = (catId) => {
|
||||
setSelectedCategory(catId)
|
||||
setHistoryVisible(!!catId)
|
||||
}
|
||||
|
||||
const onEditorClosed = useCallback(() => {
|
||||
setEditorVisible(false)
|
||||
updateData()
|
||||
}, [updateData])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Layout>
|
||||
{data && (
|
||||
<div style={{ border: '1px solid #BEBEBE' }}>
|
||||
<div style={{ padding: '5px 10px', backgroundColor: '#F3F3F3', borderBottom: '1px solid #BEBEBE' }}>Программа бурения</div>
|
||||
<Flex style={{ justifyContent: 'flex-start', alignItems: 'center', backgroundColor: 'white' }}>
|
||||
<Button type={'link'} icon={<FileWordOutlined />} style={{ marginLeft: '10px' }}>{data.name}</Button>
|
||||
<div style={{ margin: '10px' }}>Размер: {data.size}</div>
|
||||
<div style={{ margin: '10px' }}>Загружен: {formatDate(data.uploadDate)}</div>
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.parts?.map?.((part, idx) => part && (
|
||||
<CategoryRender
|
||||
key={`${idx}`}
|
||||
idWell={idWell}
|
||||
partData={part}
|
||||
onEdit={onCategoryEdit}
|
||||
onUpdate={updateData}
|
||||
onHistory={onCategoryHistory}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CategoryAdder
|
||||
idWell={idWell}
|
||||
categories={categories}
|
||||
onUpdate={updateData}
|
||||
/>
|
||||
|
||||
<CategoryEditor
|
||||
idWell={idWell}
|
||||
visible={editorVisible}
|
||||
onClosed={onEditorClosed}
|
||||
category={data?.parts?.find((part) => part.idFileCategory === selectedCategory) ?? {}}
|
||||
/>
|
||||
|
||||
<CategoryHistory
|
||||
idWell={idWell}
|
||||
idCategory={selectedCategory}
|
||||
onClose={() => setHistoryVisible(false)}
|
||||
visible={historyVisible}
|
||||
/>
|
||||
</Layout>
|
||||
</LoaderPortal>
|
||||
)
|
||||
})
|
||||
|
||||
export default DrillingProgram
|
137
src/styles/drilling_program.less
Normal file
137
src/styles/drilling_program.less
Normal file
@ -0,0 +1,137 @@
|
||||
@border-style: 1px solid #BEBEBE;
|
||||
@header-bg: #F3F3F3;
|
||||
@content-bg: white;
|
||||
|
||||
@approve-users-flex-width: 6;
|
||||
@approve-panel-flex-width: 3;
|
||||
|
||||
.ml-15 { margin-left: 15px; }
|
||||
.mv-5 { margin: 5px 0; }
|
||||
|
||||
.drilling_category {
|
||||
margin-top: 10px;
|
||||
border: @border-style;
|
||||
|
||||
> .category_header {
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: @border-style;
|
||||
|
||||
> h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> div > * {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
> .category_content {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background-color: @content-bg;
|
||||
.drilling-category-column();
|
||||
}
|
||||
|
||||
> .file_column {
|
||||
> .file_info {
|
||||
flex: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
> .file_actions {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approve_column {
|
||||
> .user_list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: @approve-users-flex-width;
|
||||
|
||||
> span { margin: 5px 10px; }
|
||||
}
|
||||
|
||||
> .approve_list,
|
||||
> .reject_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: @approve-panel-flex-width;
|
||||
margin: 0 5px;
|
||||
|
||||
> .panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
flex-grow: 1;
|
||||
|
||||
> .panel_title {
|
||||
margin: 5px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
> .panel_content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
flex-grow: 1;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .approve_list > .panel {
|
||||
background-color: #EFFEEF;
|
||||
border: 1px solid #1FB448;
|
||||
}
|
||||
|
||||
> .reject_list > .panel {
|
||||
background-color: #FEF2EF;
|
||||
border: 1px solid #B4661F;
|
||||
}
|
||||
}
|
||||
|
||||
.history_approve_column {
|
||||
.approve_column();
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.category_adder {
|
||||
display: flex;
|
||||
margin: 5px;
|
||||
|
||||
> .adder_select {
|
||||
flex-grow: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Миксин для столбцов сетки (размер, отступы, границы) */
|
||||
.drilling-category-column(@first: 4, @last: 8, @padding: 5px 10px) {
|
||||
padding: @padding;
|
||||
&:first-child { flex: @first; } // Относительная ширина первого столбца
|
||||
&:last-child { flex: @last; } // Относительная ширина второго столбца
|
||||
&:not(:last-child) { border-right: @border-style; }
|
||||
}
|
Loading…
Reference in New Issue
Block a user