фильтры
This commit is contained in:
@@ -23,30 +23,82 @@ import {
|
||||
SidebarTrigger,
|
||||
} from '@/component/ui/sidebar'
|
||||
import { Textarea } from '@/component/ui/textarea'
|
||||
import {
|
||||
apiTemplateWithAttributesToTemplate,
|
||||
getTemplateWithAttributesApi,
|
||||
} from '@/entitiy/template/api/templateApiService'
|
||||
import { useCategoryContext } from '@/entitiy/template/model/CategoryContext'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { Template, TemplateAttributeDetail } from '@/type/template'
|
||||
import TemplateCard from '@/widget/template/ui/TemplateCard'
|
||||
import clsx from 'clsx'
|
||||
import { CheckSquare, Plus, Square, Trash2 } 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,
|
||||
}))
|
||||
}
|
||||
|
||||
export const TemplatesOverviewPage = () => {
|
||||
const { templates, deleteTemplate, addTemplate } = useTemplateContext()
|
||||
const {
|
||||
templates,
|
||||
deleteTemplate,
|
||||
addTemplate,
|
||||
copyTemplate,
|
||||
updateTemplate,
|
||||
} = useTemplateContext()
|
||||
const { isLoading: attributesLoading } = useCategoryContext()
|
||||
|
||||
const [selected, setSelected] = useState<Set<string>>(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<string | null>(null)
|
||||
const [templateToEdit, setTemplateToEdit] = useState<string | null>(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<TemplateFilters>({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -61,6 +113,29 @@ export const TemplatesOverviewPage = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Функция для загрузки атрибутов шаблона
|
||||
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())
|
||||
@@ -82,6 +157,87 @@ export const TemplatesOverviewPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -111,9 +267,40 @@ export const TemplatesOverviewPage = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
// Примечание: Фильтрация по атрибутам пока отключена,
|
||||
// так как атрибуты теперь хранятся отдельно от шаблонов
|
||||
// и требуют отдельного запроса к API для каждого шаблона
|
||||
// Фильтрация по атрибутам
|
||||
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
|
||||
})
|
||||
@@ -222,8 +409,14 @@ export const TemplatesOverviewPage = () => {
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={newAttributeValues}
|
||||
onValuesChange={setNewAttributeValues}
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
newAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setNewAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -237,6 +430,154 @@ export const TemplatesOverviewPage = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Диалог копирования */}
|
||||
<Dialog
|
||||
open={isCopyDialogOpen}
|
||||
onOpenChange={setIsCopyDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Копировать шаблон</DialogTitle>
|
||||
<DialogDescription>
|
||||
Укажите название и описание для копии шаблона
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="copy-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="copy-name"
|
||||
className="col-span-3"
|
||||
value={copyName}
|
||||
onChange={e => setCopyName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="copy-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="copy-description"
|
||||
className="col-span-3"
|
||||
value={copyDescription}
|
||||
onChange={e => setCopyDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label className="self-start pt-2 text-right">
|
||||
Атрибуты
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
copyAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setCopyAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={handleCopyConfirm}
|
||||
disabled={!copyName.trim()}
|
||||
>
|
||||
Копировать
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Диалог редактирования */}
|
||||
<Dialog
|
||||
open={isEditDialogOpen}
|
||||
onOpenChange={setIsEditDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать шаблон</DialogTitle>
|
||||
<DialogDescription>
|
||||
Измените название и описание шаблона
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
className="col-span-3"
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="edit-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="edit-description"
|
||||
className="col-span-3"
|
||||
value={editDescription}
|
||||
onChange={e => setEditDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Label className="self-start pt-2 text-right">
|
||||
Атрибуты
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
{!attributesLoading && (
|
||||
<CategoryFieldsSelector
|
||||
selectedValues={toCategoryAttributeValues(
|
||||
editAttributeValues
|
||||
)}
|
||||
onValuesChange={values =>
|
||||
setEditAttributeValues(
|
||||
fromCategoryAttributeValues(values)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={handleEditConfirm}
|
||||
disabled={!editName.trim()}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -274,6 +615,10 @@ export const TemplatesOverviewPage = () => {
|
||||
selectionMode={selectionMode}
|
||||
isSelected={selected.has(t.id)}
|
||||
onToggleSelect={toggleSelect}
|
||||
onCopy={handleCopy}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
activeFilters={filters.attributes}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user