diff --git a/src/pages/DrillingProgram.jsx b/src/pages/DrillingProgram.jsx
deleted file mode 100644
index b19a8bf..0000000
--- a/src/pages/DrillingProgram.jsx
+++ /dev/null
@@ -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 (
-
-
- {program && (
-
-
Программа бурения
-
- } style={{ marginLeft: '10px' }}>{program.name}
- Размер: {program.size}
- Загружен: {formatDate(program.uploadDate)}
-
-
- )}
-
- {categories.map((category) => category && (
-
-
- {category.caption}
- Согласованты
-
-
-
-
- }>{category.file.name}
- Автор:
- Размер: {category.file.size}
- Загружен: {formatDate(category.file.uploadDate)}
-
-
- } style={{ margin: '5px 0 10px 0' }} title={'Загрузить'}>Загрузить
- } disabled={!category.file} title={'История'}>История
-
-
-
-
- {category.undecided.map((user, i) => (
-
-
-
- ))}
-
-
- {category.status === 0 && hasPermission() && (
-
- )}
-
-
- Согласовано
- {formatDate(category.lastApprove)}
-
-
- {category.approved?.map(({ comment, user }, i) => (
-
-
-
-
-
-
- ))}
-
-
-
-
- {category.status === 0 && hasPermission() && (
-
- )}
-
-
- Отклонено
- {formatDate(category.lastReject)}
-
-
- {category.rejected?.map(({ comment, user }, i) => (
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
- ))}
-
-
- )
-})
-
-export default DrillingProgram
diff --git a/src/pages/DrillingProgram/CategoryAdder.jsx b/src/pages/DrillingProgram/CategoryAdder.jsx
new file mode 100644
index 0000000..c180457
--- /dev/null
+++ b/src/pages/DrillingProgram/CategoryAdder.jsx
@@ -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 (
+
+
+ }
+ disabled={!value || value.length <= 0}
+ >
+ Добавить
+
+
+ )
+})
+
+export default CategoryAdder
diff --git a/src/pages/DrillingProgram/CategoryEditor.jsx b/src/pages/DrillingProgram/CategoryEditor.jsx
new file mode 100644
index 0000000..b9568bd
--- /dev/null
+++ b/src/pages/DrillingProgram/CategoryEditor.jsx
@@ -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,
+ <>
+ Не удалось изменить статус пользователя
+
+ >,
+ `Изменение статуса пользователя`
+ ), [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) => ,
+ }),
+ makeColumn('Статус', 'status', {
+ sorter: makeNumericSorter('status'),
+ defaultSortOrder: 'descend',
+ render: (status, { user }) => (
+ 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 (
+
+
+
onSearchTextChange(e?.target?.value)}
+ onSearch={(text) => calcFilteredUsers(text, users)}
+ value={searchValue}
+ style={{ marginBottom: '15px' }}
+ />
+
+
+
+
+
+ )
+})
+
+export default CategoryEditor
diff --git a/src/pages/DrillingProgram/CategoryHistory.jsx b/src/pages/DrillingProgram/CategoryHistory.jsx
new file mode 100644
index 0000000..bfd1047
--- /dev/null
+++ b/src/pages/DrillingProgram/CategoryHistory.jsx
@@ -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) =>
+ }),
+ makeColumn('Компания', 'author', {
+ sorter: (a, b) => makeStringSorter('caption')(a?.author?.company, b?.author?.company),
+ render: (author) =>
+ }),
+ 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 (
+
+ )
+ }
+ })
+]
+
+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 (
+ Закрыть
+ )}
+ >
+
+
+
+ setFileName(value)}
+ />
+ setCompanyName(value)}
+ />
+ setRange(range)} />
+
+
+
+
+
+ )
+}
+
+export default CategoryHistory
diff --git a/src/pages/DrillingProgram/CategoryRender.jsx b/src/pages/DrillingProgram/CategoryRender.jsx
new file mode 100644
index 0000000..2f31cb5
--- /dev/null
+++ b/src/pages/DrillingProgram/CategoryRender.jsx
@@ -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) => (
+
+
+
+
+
+))
+
+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 (
+
+
+
{title}
+
+
} onClick={() => onEdit?.(idFileCategory)}>Редактировать
+
Вы уверены, что хотите удалить категорию
"{title}"?>}
+ >
+ }>Удалить
+
+
+
+
+
+
+ {file ? (
+ <>
+
+
Автор:
+
Размер: {file.size ? formatBytes(file.size) : '-'}
+
Загружен: {formatDate(file.uploadDate) ?? '-'}
+ >
+ ) : (
+
Нет загруженных файлов
+ )}
+
+
+ setIsUploading(true)}
+ onUploadComplete={() => onUpdate?.(idFileCategory)}
+ onUploadError={(e) => notify(e?.message ?? 'Ошибка загрузки файла', 'error')}
+ />
+ }
+ onClick={() => onHistory?.(idFileCategory)}
+ >
+ История
+
+
+
+
+
+ {approvers.map((user, i) => (
+
+
+
+ ))}
+
+
+ {permissionToApprove && (
+
+ )}
+ mark.idMarkType === 1)} />
+
+
+ {permissionToApprove && (
+
+ )}
+ mark.idMarkType === 0)} />
+
+
+
+
+ )
+})
+
+export default CategoryRender
diff --git a/src/pages/DrillingProgram/MarksCard.jsx b/src/pages/DrillingProgram/MarksCard.jsx
new file mode 100644
index 0000000..71cfa3f
--- /dev/null
+++ b/src/pages/DrillingProgram/MarksCard.jsx
@@ -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 }) => (
+
+
{title}
+
+ {marks?.map(({ dateCreated, comment, user }, i) => (
+
+
+ {formatDate(dateCreated)}
+
+
+
+
+ ))}
+
+
+))
+
+export default MarksCard
diff --git a/src/pages/DrillingProgram/index.jsx b/src/pages/DrillingProgram/index.jsx
new file mode 100644
index 0000000..b42020a
--- /dev/null
+++ b/src/pages/DrillingProgram/index.jsx
@@ -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 (
+
+
+ {data && (
+
+
Программа бурения
+
+ } style={{ marginLeft: '10px' }}>{data.name}
+ Размер: {data.size}
+ Загружен: {formatDate(data.uploadDate)}
+
+
+ )}
+
+ {data?.parts?.map?.((part, idx) => part && (
+
+ ))}
+
+
+
+ part.idFileCategory === selectedCategory) ?? {}}
+ />
+
+ setHistoryVisible(false)}
+ visible={historyVisible}
+ />
+
+
+ )
+})
+
+export default DrillingProgram
diff --git a/src/styles/drilling_program.less b/src/styles/drilling_program.less
new file mode 100644
index 0000000..324de30
--- /dev/null
+++ b/src/styles/drilling_program.less
@@ -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; }
+}