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 (
{/* Статистика фильтрации */}
{(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