import { CategoryFieldsSelector } from '@/component/TemplateManager/CategoryFieldsSelector' import { TemplateFilterSidebar, TemplateFilters, } from '@/component/TemplateManager/TemplateFilterSidebar' import { Button } from '@/component/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/component/ui/dialog' import { Input } from '@/component/ui/input' import { Separator } from '@/component/ui/separator' import { SidebarInset, SidebarProvider, SidebarTrigger, } from '@/component/ui/sidebar' import { apiTemplateWithAttributesToTemplate, getTemplateWithAttributesApi, } from '@/entity/template/api/templateApiService' import { useCategoryContext } from '@/entity/template/model/CategoryContext' import { useTemplateContext } from '@/entity/template/model/TemplateContext' import { Template, TemplateAttributeDetail } from '@/type/template' import TemplateCard from '@/widget/template/ui/TemplateCard' import clsx from 'clsx' import { CheckSquare, Copy, Edit3, Plus, Square, Tag, Trash2, Type, } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' interface AttributeValue { attributeId: string value: string | null } // Тип для CategoryFieldsSelector interface CategoryAttributeValue { attributeId: string value: string | number } // Адаптеры для преобразования типов const toCategoryAttributeValues = ( values: AttributeValue[] ): CategoryAttributeValue[] => { return values.map(v => ({ attributeId: v.attributeId, value: v.value || '', })) } const fromCategoryAttributeValues = ( values: CategoryAttributeValue[] ): AttributeValue[] => { return values.map(v => ({ attributeId: v.attributeId, value: v.value?.toString() || null, })) } // Компактный селектор атрибутов interface CompactCategoryFieldsSelectorProps { selectedValues: CategoryAttributeValue[] onValuesChange: (values: CategoryAttributeValue[]) => void isLoading?: boolean } const CompactCategoryFieldsSelector: React.FC< CompactCategoryFieldsSelectorProps > = ({ selectedValues, onValuesChange, isLoading = false }) => { if (isLoading) { return (
Загрузка атрибутов...
) } return (
) } // Общий компонент формы шаблона interface TemplateFormProps { title: string description: string icon: React.ReactNode iconColor: string name: string onNameChange: (value: string) => void templateDescription: string onDescriptionChange: (value: string) => void attributeValues: AttributeValue[] onAttributeValuesChange: (values: AttributeValue[]) => void attributesLoading: boolean onSubmit: () => void | Promise onCancel: () => void submitText: string submitIcon: React.ReactNode isSubmitDisabled: boolean nameLabel?: string descriptionLabel?: string } const TemplateForm: React.FC = ({ title, description, icon, iconColor, name, onNameChange, templateDescription, onDescriptionChange, attributeValues, onAttributeValuesChange, attributesLoading, onSubmit, onCancel, submitText, submitIcon, isSubmitDisabled, nameLabel = 'Название шаблона', descriptionLabel = 'Описание', }) => { return ( <> {icon} {title} {description}
{/* Основные настройки */}
{nameLabel}
onNameChange(e.target.value)} className="h-9" />
{/* Атрибуты */}
Атрибуты шаблона
onAttributeValuesChange(fromCategoryAttributeValues(values)) } isLoading={attributesLoading} />
{!name.trim() && ( Заполните {nameLabel.toLowerCase()} )}
) } export const TemplatesOverviewPage = () => { const { templates, deleteTemplate, addTemplate, copyTemplate, updateTemplate, } = useTemplateContext() const { isLoading: attributesLoading } = useCategoryContext() const [selected, setSelected] = useState>(new Set()) const [selectionMode, setSelectionMode] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false) const [isCopyDialogOpen, setIsCopyDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [templateToCopy, setTemplateToCopy] = useState(null) const [templateToEdit, setTemplateToEdit] = useState(null) const [newName, setNewName] = useState('') const [newDescription, setNewDescription] = useState('') const [newAttributeValues, setNewAttributeValues] = useState< AttributeValue[] >([]) const [copyName, setCopyName] = useState('') const [copyDescription, setCopyDescription] = useState('') const [copyAttributeValues, setCopyAttributeValues] = useState< AttributeValue[] >([]) const [editName, setEditName] = useState('') const [editDescription, setEditDescription] = useState('') const [editAttributeValues, setEditAttributeValues] = useState< AttributeValue[] >([]) const [templateAttributesLoading, setTemplateAttributesLoading] = useState(false) const [filters, setFilters] = useState({ name: '', description: '', attributes: {}, }) const toggleSelect = useCallback((id: string) => { setSelected(prev => { const next = new Set(prev) next.has(id) ? next.delete(id) : next.add(id) return next }) }, []) // Функция для загрузки атрибутов шаблона const loadTemplateAttributes = async (templateId: string) => { try { setTemplateAttributesLoading(true) const { template } = await getTemplateWithAttributesApi(templateId) if (template) { const templateWithAttrs = apiTemplateWithAttributesToTemplate(template) const attributeValues: AttributeValue[] = templateWithAttrs.attributes.map(attr => ({ attributeId: attr.attribute_id, value: attr.value, })) return attributeValues } return [] } catch (error) { console.error('Ошибка при загрузке атрибутов шаблона:', error) return [] } finally { setTemplateAttributesLoading(false) } } const removeSelected = () => { selected.forEach(deleteTemplate) setSelected(new Set()) setSelectionMode(false) } const handleCreate = async () => { if (newName.trim()) { try { // Создаем шаблон с атрибутами сразу await addTemplate( { name: newName, elements: [], description: newDescription, mergedCells: [], }, newAttributeValues.length > 0 ? newAttributeValues : undefined ) setNewName('') setNewDescription('') setNewAttributeValues([]) setIsDialogOpen(false) } catch (error) { console.error('Ошибка при создании шаблона:', error) } } } const handleCopy = (templateId: string) => { const template = templates.find(t => t.id === templateId) if (template) { setTemplateToCopy(templateId) setCopyName(`${template.name} (копия)`) setCopyDescription(template.description || '') // Загружаем атрибуты шаблона loadTemplateAttributes(templateId).then(attributes => { setCopyAttributeValues(attributes) }) setIsCopyDialogOpen(true) } } const handleCopyConfirm = async () => { if (templateToCopy && copyName.trim()) { try { await copyTemplate( templateToCopy, copyName, copyDescription, copyAttributeValues ) setCopyName('') setCopyDescription('') setCopyAttributeValues([]) setTemplateToCopy(null) setIsCopyDialogOpen(false) } catch (error) { console.error('Ошибка при копировании шаблона:', error) } } } const handleEdit = (templateId: string) => { const template = templates.find(t => t.id === templateId) if (template) { setTemplateToEdit(templateId) setEditName(template.name) setEditDescription(template.description || '') // Загружаем атрибуты шаблона loadTemplateAttributes(templateId).then(attributes => { setEditAttributeValues(attributes) }) setIsEditDialogOpen(true) } } const handleEditConfirm = async () => { if (templateToEdit && editName.trim()) { try { const template = templates.find(t => t.id === templateToEdit) if (template) { const updatedTemplate = { ...template, name: editName, description: editDescription, } await updateTemplate(updatedTemplate, editAttributeValues) setEditName('') setEditDescription('') setEditAttributeValues([]) setTemplateToEdit(null) setIsEditDialogOpen(false) } } catch (error) { console.error('Ошибка при редактировании шаблона:', error) } } } const handleDelete = (templateId: string) => { if (confirm('Вы уверены, что хотите удалить этот шаблон?')) { deleteTemplate(templateId) } } const toggleSelectionMode = () => { setSelectionMode(v => { if (v) setSelected(new Set()) return !v }) } // Фильтрация шаблонов const filteredTemplates = useMemo(() => { return templates.filter(template => { // Фильтр по названию if ( filters.name && !template.name.toLowerCase().includes(filters.name.toLowerCase()) ) { return false } // Фильтр по описанию if ( filters.description && (!template.description || !template.description .toLowerCase() .includes(filters.description.toLowerCase())) ) { return false } // Фильтрация по атрибутам if (Object.keys(filters.attributes).length > 0) { // Проверяем есть ли атрибуты у шаблона const templateWithAttrs = template as Template & { attributes?: TemplateAttributeDetail[] } if (!templateWithAttrs.attributes) { return false } // Проверяем соответствуют ли атрибуты фильтрам for (const [attributeId, filterValue] of Object.entries( filters.attributes )) { if (!filterValue) continue // пропускаем пустые фильтры const templateAttribute = templateWithAttrs.attributes.find( (attr: TemplateAttributeDetail) => attr.attribute_id === attributeId ) if (!templateAttribute || !templateAttribute.value) { return false // атрибут не найден или пустой } // Проверяем содержится ли значение фильтра в значении атрибута if ( !templateAttribute.value .toLowerCase() .includes(filterValue.toLowerCase()) ) { return false } } } return true }) }, [templates, filters]) return (

Карточки протоколов

{selectionMode && ( <> Выбрано: {selected.size} )} {!selectionMode && ( <> {/* Диалог создания шаблона */} } iconColor="text-blue-600" name={newName} onNameChange={setNewName} templateDescription={newDescription} onDescriptionChange={setNewDescription} attributeValues={newAttributeValues} onAttributeValuesChange={setNewAttributeValues} attributesLoading={attributesLoading} onSubmit={handleCreate} onCancel={() => setIsDialogOpen(false)} submitText="Создать" submitIcon={} isSubmitDisabled={!newName.trim()} /> {/* Диалог копирования */} } iconColor="text-green-600" name={copyName} onNameChange={setCopyName} templateDescription={copyDescription} onDescriptionChange={setCopyDescription} attributeValues={copyAttributeValues} onAttributeValuesChange={setCopyAttributeValues} attributesLoading={attributesLoading} onSubmit={handleCopyConfirm} onCancel={() => setIsCopyDialogOpen(false)} submitText="Копировать" submitIcon={} isSubmitDisabled={!copyName.trim()} nameLabel="Название копии" descriptionLabel="Описание копии" /> {/* Диалог редактирования */} } iconColor="text-orange-600" name={editName} onNameChange={setEditName} templateDescription={editDescription} onDescriptionChange={setEditDescription} attributeValues={editAttributeValues} onAttributeValuesChange={setEditAttributeValues} attributesLoading={attributesLoading} onSubmit={handleEditConfirm} onCancel={() => setIsEditDialogOpen(false)} submitText="Сохранить" submitIcon={} isSubmitDisabled={!editName.trim()} /> )}
{/* Статистика фильтрации */} {(filters.name || filters.description || Object.keys(filters.attributes).length > 0) && (
Показано {filteredTemplates.length} из {templates.length}{' '} шаблонов {Object.keys(filters.attributes).length > 0 && ( • Фильтры по атрибутам:{' '} {Object.keys(filters.attributes).length} )}
)}
{filteredTemplates.map(t => ( ))}
{filteredTemplates.length === 0 && templates.length > 0 && (

Шаблоны не найдены

Попробуйте изменить фильтры поиска

)} {templates.length === 0 && (

Пока нет шаблонов

Создайте свой первый шаблон протокола

)}
) } export default TemplatesOverviewPage