-
- {element.required && (
-
+ {/* Показываем label и индикатор ячеек только если элемент не использует свой Render компонент */}
+ {!usesElementRender && (
+
+
+
+ {element.required && (
+
+ )}
+
+ {/* Индикатор ячеек */}
+ {element.targetCells && element.targetCells.length > 0 && (
+
+
+
+
Целевые ячейки:
+ {element.targetCells.map((cell, i) => (
+
+ {cell.sheet}!{cell.cell}
+ {cell.displayName && (
+
+ ({cell.displayName})
+
+ )}
+
+ ))}
+
+
)}
- {/* Индикатор ячеек */}
- {element.targetCells && element.targetCells.length > 0 && (
-
-
-
-
Целевые ячейки:
- {element.targetCells.map((cell, i) => (
-
- {cell.sheet}!{cell.cell}
- {cell.displayName && (
-
- ({cell.displayName})
-
- )}
-
- ))}
+ )}
+
+ {/* Для элементов с собственными Render компонентами показываем только индикатор ячеек */}
+ {usesElementRender &&
+ element.targetCells &&
+ element.targetCells.length > 0 && (
+
+
+
+
+
Целевые ячейки:
+ {element.targetCells.map((cell, i) => (
+
+ {cell.sheet}!{cell.cell}
+ {cell.displayName && (
+
+ ({cell.displayName})
+
+ )}
+
+ ))}
+
)}
-
+
{renderInput()}
)
@@ -388,7 +436,7 @@ const ProtocolForm: FC
= ({ template, onSave, onBack }) => {
// Специальная обработка для условий калибровки
if (
- element.type === 'calibration-conditions' &&
+ element.type === 'calibration_conditions' &&
typeof value === 'object' &&
value !== null
) {
@@ -403,6 +451,15 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => {
cellValue = (value as any)[propsOrder[idx]] ?? ''
}
+ // Специальная обработка для группы кнопок
+ if (element.type === 'button_group' && typeof value === 'string') {
+ // Найдем соответствующий option для получения label
+ const buttonOption = element.options?.find(
+ opt => opt.value === value
+ )
+ cellValue = buttonOption ? buttonOption.label : value
+ }
+
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
@@ -497,7 +554,7 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => {
// 2) Объект условия калибровки -> пишем свойства по порядку
if (
- el.type === 'calibration-conditions' &&
+ el.type === 'calibration_conditions' &&
typeof value === 'object' &&
value !== null
) {
@@ -524,7 +581,25 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => {
return
}
- // 3) Примитив или прочие объекты -> одно значение во все ячейки
+ // 3) Группа кнопок -> записываем label выбранной кнопки
+ if (el.type === 'button_group' && typeof value === 'string') {
+ const buttonOption = el.options?.find(opt => opt.value === value)
+ const cellValue = buttonOption ? buttonOption.label : value
+ cells.forEach(tc => {
+ const { row, col } = cellAddressToCoordinates(tc.cell)
+ const sheetName =
+ tc.sheet === 'R' || tc.sheet === 'Calculations' ? 'R' : 'L'
+ engineRef.current.setCellValueWithoutRecalc(
+ sheetName,
+ row,
+ col,
+ cellValue
+ )
+ })
+ return
+ }
+
+ // 4) Примитив или прочие объекты -> одно значение во все ячейки
cells.forEach(tc => {
const { row, col } = cellAddressToCoordinates(tc.cell)
const sheetName =
@@ -579,6 +654,33 @@ const ProtocolForm: FC = ({ template, onSave, onBack }) => {
})
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
+
+ // Получаем файл как blob и скачиваем его
+ const blob = await resp.blob()
+ const url = window.URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+
+ // Получаем имя файла из заголовка Content-Disposition или создаем по умолчанию
+ const contentDisposition = resp.headers.get('content-disposition')
+ let filename = `protocol_${template.name}_${new Date().toISOString().split('T')[0]}.xlsx`
+
+ if (contentDisposition) {
+ const matches = contentDisposition.match(
+ /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+ )
+ if (matches && matches[1]) {
+ filename = matches[1].replace(/['"]/g, '')
+ }
+ }
+
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ window.URL.revokeObjectURL(url)
+ document.body.removeChild(a)
+
+ toast.success('Протокол создан и загружен!')
} catch (err) {
console.error('Ошибка при создании протокола', err)
toast.error('Ошибка при создании протокола')
diff --git a/src/page/TemplatesOverviewPage.tsx b/src/page/TemplatesOverviewPage.tsx
index 6e37f2d..a48af2b 100644
--- a/src/page/TemplatesOverviewPage.tsx
+++ b/src/page/TemplatesOverviewPage.tsx
@@ -1,3 +1,8 @@
+import { CategoryFieldsSelector } from '@/component/TemplateManager/CategoryFieldsSelector'
+import {
+ TemplateFilterSidebar,
+ TemplateFilters,
+} from '@/component/TemplateManager/TemplateFilterSidebar'
import { Button } from '@/component/ui/button'
import {
Dialog,
@@ -11,23 +16,42 @@ import {
} from '@/component/ui/dialog'
import { Input } from '@/component/ui/input'
import { Label } from '@/component/ui/label'
+import { Separator } from '@/component/ui/separator'
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from '@/component/ui/sidebar'
import { Textarea } from '@/component/ui/textarea'
+import { useCategoryContext } from '@/entitiy/template/model/CategoryContext'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import TemplateCard from '@/widget/template/ui/TemplateCard'
import clsx from 'clsx'
import { CheckSquare, Plus, Square, Trash2 } from 'lucide-react'
-import { useCallback, useState } from 'react'
+import { useCallback, useMemo, useState } from 'react'
+
+interface AttributeValue {
+ attributeId: string
+ value: string | number
+}
export const TemplatesOverviewPage = () => {
const { templates, deleteTemplate, addTemplate } = useTemplateContext()
+ const { isLoading: attributesLoading } = useCategoryContext()
const [selected, setSelected] = useState>(new Set())
-
const [selectionMode, setSelectionMode] = useState(false)
-
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [newName, setNewName] = useState('')
const [newDescription, setNewDescription] = useState('')
+ const [newAttributeValues, setNewAttributeValues] = useState<
+ AttributeValue[]
+ >([])
+ const [filters, setFilters] = useState({
+ name: '',
+ description: '',
+ attributes: {},
+ })
const toggleSelect = useCallback((id: string) => {
setSelected(prev => {
@@ -52,6 +76,8 @@ export const TemplatesOverviewPage = () => {
mergedCells: [],
})
setNewName('')
+ setNewDescription('')
+ setNewAttributeValues([])
setIsDialogOpen(false)
}
}
@@ -63,99 +89,215 @@ export const TemplatesOverviewPage = () => {
})
}
- return (
-
-