Единый интерфейс элементов
This commit is contained in:
@@ -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<Set<string>>(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<TemplateFilters>({
|
||||
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 (
|
||||
<main className="mx-auto max-w-7xl p-6">
|
||||
<header className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Шаблоны протоколов</h1>
|
||||
<div className="flex gap-3">
|
||||
{!!selected.size && (
|
||||
<Button variant="destructive" onClick={removeSelected}>
|
||||
<Trash2 className="mr-2 h-4 w-4" /> {selected.size}
|
||||
</Button>
|
||||
)}
|
||||
// Фильтрация шаблонов
|
||||
const filteredTemplates = useMemo(() => {
|
||||
return templates.filter(template => {
|
||||
// Фильтр по названию
|
||||
if (
|
||||
filters.name &&
|
||||
!template.name.toLowerCase().includes(filters.name.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
<Button
|
||||
variant={selectionMode ? 'secondary' : 'outline'}
|
||||
onClick={toggleSelectionMode}
|
||||
>
|
||||
{selectionMode ? (
|
||||
// Фильтр по описанию
|
||||
if (
|
||||
filters.description &&
|
||||
(!template.description ||
|
||||
!template.description
|
||||
.toLowerCase()
|
||||
.includes(filters.description.toLowerCase()))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Примечание: Фильтрация по атрибутам пока отключена,
|
||||
// так как атрибуты теперь хранятся отдельно от шаблонов
|
||||
// и требуют отдельного запроса к API для каждого шаблона
|
||||
|
||||
return true
|
||||
})
|
||||
}, [templates, filters])
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={true}>
|
||||
<TemplateFilterSidebar
|
||||
templates={templates}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
<SidebarInset>
|
||||
<header className="flex items-center gap-2 border-b px-4 py-2">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<h1 className="text-lg font-semibold">Шаблоны</h1>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{selectionMode && (
|
||||
<>
|
||||
<Square className="mr-2 h-4 w-4" /> Отменить
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckSquare className="mr-2 h-4 w-4" /> Выбрать
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Выбрано: {selected.size}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={removeSelected}
|
||||
disabled={selected.size === 0}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
Удалить
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={toggleSelectionMode}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Создать
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<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="template-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="template-name"
|
||||
className="col-span-3"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
placeholder="Введите название"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="template-description" className="text-right">
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="template-description"
|
||||
className="col-span-3"
|
||||
value={newDescription}
|
||||
onChange={e => setNewDescription(e.target.value)}
|
||||
placeholder="Введите описание"
|
||||
/>
|
||||
</div>
|
||||
{!selectionMode && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={toggleSelectionMode}
|
||||
disabled={templates.length === 0}
|
||||
>
|
||||
{selected.size === templates.length ? (
|
||||
<Square className="mr-1 h-4 w-4" />
|
||||
) : (
|
||||
<CheckSquare className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
Выбрать
|
||||
</Button>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
Создать
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<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="template-name" className="text-right">
|
||||
Название
|
||||
</Label>
|
||||
<Input
|
||||
id="template-name"
|
||||
className="col-span-3"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
placeholder="Введите название шаблона"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="template-description"
|
||||
className="text-right"
|
||||
>
|
||||
Описание
|
||||
</Label>
|
||||
<Textarea
|
||||
id="template-description"
|
||||
className="col-span-3"
|
||||
value={newDescription}
|
||||
onChange={e => setNewDescription(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={newAttributeValues}
|
||||
onValuesChange={setNewAttributeValues}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleCreate}>Создать</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 p-6">
|
||||
{/* Статистика фильтрации */}
|
||||
{(filters.name ||
|
||||
filters.description ||
|
||||
Object.keys(filters.attributes).length > 0) && (
|
||||
<div className="mb-4 rounded-lg bg-muted/50 p-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Показано {filteredTemplates.length} из {templates.length}{' '}
|
||||
шаблонов
|
||||
{Object.keys(filters.attributes).length > 0 && (
|
||||
<span className="ml-2">
|
||||
• Фильтры по атрибутам:{' '}
|
||||
{Object.keys(filters.attributes).length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Отмена</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleCreate}>Создать</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section
|
||||
className={clsx(
|
||||
'grid gap-6',
|
||||
'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
||||
)}
|
||||
>
|
||||
{templates.map(t => (
|
||||
<TemplateCard
|
||||
key={t.id}
|
||||
template={t}
|
||||
selectionMode={selectionMode}
|
||||
isSelected={selected.has(t.id)}
|
||||
onToggleSelect={toggleSelect}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</main>
|
||||
<section
|
||||
className={clsx(
|
||||
'grid gap-6',
|
||||
'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
||||
)}
|
||||
>
|
||||
{filteredTemplates.map(t => (
|
||||
<TemplateCard
|
||||
key={t.id}
|
||||
template={t}
|
||||
selectionMode={selectionMode}
|
||||
isSelected={selected.has(t.id)}
|
||||
onToggleSelect={toggleSelect}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{filteredTemplates.length === 0 && templates.length > 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="text-muted-foreground">
|
||||
<p className="text-lg font-medium">Шаблоны не найдены</p>
|
||||
<p className="text-sm">Попробуйте изменить фильтры поиска</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{templates.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="text-muted-foreground">
|
||||
<p className="text-lg font-medium">Пока нет шаблонов</p>
|
||||
<p className="text-sm">Создайте свой первый шаблон протокола</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user