Программа бурения разделена на директорию и завершена

This commit is contained in:
Александр Сироткин 2022-02-22 15:30:20 +05:00
parent 7a33c5ef2f
commit 2c44899212
8 changed files with 778 additions and 236 deletions

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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; }
}