Единый интерфейс элементов

This commit is contained in:
2025-07-22 19:44:49 +03:00
parent 553da77a87
commit 3fc352f8e9
36 changed files with 2108 additions and 675 deletions

View File

@@ -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>
)
}