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