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 && ( -
-
Программа бурения
- - -
Размер: {program.size}
-
Загружен: {formatDate(program.uploadDate)}
-
-
- )} - - {categories.map((category) => category && ( -
- -
{category.caption}
-
Согласованты
-
- - - - -
Автор:
-
Размер: {category.file.size}
-
Загружен: {formatDate(category.file.uploadDate)}
-
- - - - -
- -
- {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 ( +
+ + + +)) + +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}

+
+ + Вы уверены, что хотите удалить категорию
"{title}"?} + > + +
+
+
+
+
+
+ {file ? ( + <> + +
Автор:
+
Размер: {file.size ? formatBytes(file.size) : '-'}
+
Загружен: {formatDate(file.uploadDate) ?? '-'}
+ + ) : ( +
Нет загруженных файлов
+ )} +
+
+ setIsUploading(true)} + onUploadComplete={() => onUpdate?.(idFileCategory)} + onUploadError={(e) => notify(e?.message ?? 'Ошибка загрузки файла', 'error')} + /> + +
+
+
+
+ {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 && ( +
+
Программа бурения
+ + +
Размер: {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; } +}