359 lines
13 KiB
TypeScript
359 lines
13 KiB
TypeScript
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||
import { Template } from '@/type/template'
|
||
import TemplateCard from '@/widget/template/ui/TemplateCard'
|
||
import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
|
||
import React, { useEffect, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { Button } from 'shared/ui/button'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from 'shared/ui/dialog'
|
||
import { Input } from 'shared/ui/input'
|
||
import { TemplateEditor } from './TemplateEditor'
|
||
|
||
interface TemplateManagerProps {
|
||
template?: Template
|
||
}
|
||
|
||
export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||
template: initialTemplate,
|
||
}) => {
|
||
const navigate = useNavigate()
|
||
const {
|
||
templates,
|
||
isLoading,
|
||
error,
|
||
addTemplate,
|
||
updateTemplate,
|
||
deleteTemplate,
|
||
refetchTemplates,
|
||
} = useTemplateContext()
|
||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||
null
|
||
)
|
||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||
const [isEditMode, setIsEditMode] = useState(false)
|
||
const [newTemplateName, setNewTemplateName] = useState('')
|
||
const [newTemplateDescription, setNewTemplateDescription] = useState('')
|
||
const [isSelectionMode, setIsSelectionMode] = useState(false)
|
||
const [selectedTemplates, setSelectedTemplates] = useState<Set<string>>(
|
||
new Set()
|
||
)
|
||
const [templatesToDelete, setTemplatesToDelete] = useState<Template[]>([])
|
||
const [isCreating, setIsCreating] = useState(false)
|
||
|
||
// Автоматически открываем редактирование шаблона, если передан templateId
|
||
useEffect(() => {
|
||
if (initialTemplate) {
|
||
setSelectedTemplate(initialTemplate)
|
||
setIsEditMode(true)
|
||
}
|
||
}, [initialTemplate])
|
||
|
||
const handleCreateTemplate = async () => {
|
||
if (!newTemplateName.trim()) return
|
||
|
||
setIsCreating(true)
|
||
try {
|
||
await addTemplate({
|
||
name: newTemplateName.trim(),
|
||
description: newTemplateDescription.trim(),
|
||
elements: [],
|
||
layoutSettings: undefined,
|
||
})
|
||
setIsCreateDialogOpen(false)
|
||
setNewTemplateName('')
|
||
setNewTemplateDescription('')
|
||
await refetchTemplates()
|
||
const created = templates.find(t => t.name === newTemplateName.trim())
|
||
if (created) {
|
||
setSelectedTemplate(created)
|
||
setIsEditMode(true)
|
||
}
|
||
} catch (err) {
|
||
console.error('Error creating template:', err)
|
||
alert('Ошибка при создании шаблона. Попробуйте еще раз.')
|
||
} finally {
|
||
setIsCreating(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteTemplates = () => {
|
||
templatesToDelete.forEach(t => deleteTemplate(t.id))
|
||
setTemplatesToDelete([])
|
||
setIsSelectionMode(false)
|
||
setSelectedTemplates(new Set())
|
||
}
|
||
|
||
const toggleTemplateSelection = (id: UUID) => {
|
||
const next = new Set(selectedTemplates)
|
||
next.has(id) ? next.delete(id) : next.add(id)
|
||
setSelectedTemplates(next)
|
||
}
|
||
|
||
const handleSelectionModeToggle = () => {
|
||
if (isSelectionMode) {
|
||
setIsSelectionMode(false)
|
||
setSelectedTemplates(new Set())
|
||
setTemplatesToDelete([])
|
||
} else {
|
||
setIsSelectionMode(true)
|
||
}
|
||
}
|
||
|
||
const handleDeleteSelected = () => {
|
||
const toDelete = templates.filter(t => selectedTemplates.has(t.id))
|
||
setTemplatesToDelete(toDelete)
|
||
}
|
||
|
||
const handleRetry = () => {
|
||
refetchTemplates()
|
||
}
|
||
|
||
if (isEditMode && selectedTemplate) {
|
||
return (
|
||
<TemplateEditor
|
||
template={selectedTemplate}
|
||
onBack={() => {
|
||
setIsEditMode(false)
|
||
setSelectedTemplate(null)
|
||
navigate('/templates')
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
|
||
// Показываем ошибку, если есть
|
||
if (error && !isLoading) {
|
||
return (
|
||
<div className="flex h-screen items-center justify-center">
|
||
<div className="text-center">
|
||
<FileText className="mx-auto mb-4 h-16 w-16 text-red-400" />
|
||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||
Ошибка загрузки шаблонов
|
||
</h3>
|
||
<p className="mb-4 text-gray-600">{error.message}</p>
|
||
<Button onClick={handleRetry}>Попробовать снова</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="mx-auto max-w-7xl p-6">
|
||
<div className="mb-6 flex items-center justify-between">
|
||
<h1 className="text-2xl font-bold text-gray-900">Шаблоны протоколов</h1>
|
||
<div className="flex gap-3">
|
||
{isSelectionMode ? (
|
||
<>
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleSelectionModeToggle}
|
||
className="flex items-center gap-2"
|
||
>
|
||
Отменить выбор
|
||
</Button>
|
||
<Button
|
||
variant="destructive"
|
||
onClick={handleDeleteSelected}
|
||
disabled={selectedTemplates.size === 0}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
Удалить выбранные ({selectedTemplates.size})
|
||
</Button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleSelectionModeToggle}
|
||
disabled={templates.length === 0}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<Circle className="h-4 w-4" />
|
||
Выбрать
|
||
</Button>
|
||
<Dialog
|
||
open={isCreateDialogOpen}
|
||
onOpenChange={setIsCreateDialogOpen}
|
||
>
|
||
<DialogTrigger asChild>
|
||
<Button className="flex items-center gap-2">
|
||
<Plus className="h-4 w-4" />
|
||
Создать шаблон
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>Создать новый шаблон</DialogTitle>
|
||
<DialogDescription>
|
||
Введите основную информацию для нового шаблона протокола
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="mb-1 block text-sm font-medium">
|
||
Название шаблона
|
||
</label>
|
||
<Input
|
||
placeholder="Например: Протокол испытаний"
|
||
value={newTemplateName}
|
||
onChange={e => setNewTemplateName(e.target.value)}
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter') {
|
||
handleCreateTemplate()
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="mb-1 block text-sm font-medium">
|
||
Описание (необязательно)
|
||
</label>
|
||
<Input
|
||
placeholder="Краткое описание назначения шаблона"
|
||
value={newTemplateDescription}
|
||
onChange={e =>
|
||
setNewTemplateDescription(e.target.value)
|
||
}
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter') {
|
||
handleCreateTemplate()
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setIsCreateDialogOpen(false)}
|
||
disabled={isCreating}
|
||
>
|
||
Отменить
|
||
</Button>
|
||
<Button
|
||
onClick={handleCreateTemplate}
|
||
disabled={!newTemplateName.trim() || isCreating}
|
||
>
|
||
{isCreating ? 'Создание...' : 'Создать'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Показываем индикатор загрузки */}
|
||
{isLoading && (
|
||
<div className="flex h-64 items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent"></div>
|
||
<p className="text-gray-600">Загрузка шаблонов...</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Список шаблонов */}
|
||
{!isLoading && (
|
||
<>
|
||
{templates.length === 0 ? (
|
||
<div className="flex h-64 items-center justify-center">
|
||
<div className="text-center">
|
||
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||
Нет шаблонов
|
||
</h3>
|
||
<p className="mb-4 text-gray-600">
|
||
Создайте первый шаблон для начала работы
|
||
</p>
|
||
<Dialog
|
||
open={isCreateDialogOpen}
|
||
onOpenChange={setIsCreateDialogOpen}
|
||
>
|
||
<DialogTrigger asChild>
|
||
<Button>
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
Создать шаблон
|
||
</Button>
|
||
</DialogTrigger>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||
{templates.map(template => (
|
||
<div key={template.id} className="relative">
|
||
{isSelectionMode && (
|
||
<div className="absolute right-2 top-2 z-10">
|
||
<button
|
||
onClick={() => toggleTemplateSelection(template.id)}
|
||
className="rounded-full bg-white p-1 shadow-md"
|
||
>
|
||
{selectedTemplates.has(template.id) ? (
|
||
<CircleCheck className="h-5 w-5 text-blue-600" />
|
||
) : (
|
||
<Circle className="h-5 w-5 text-gray-400" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
<TemplateCard
|
||
template={template}
|
||
onDelete={() => deleteTemplate(template.id)}
|
||
isSelected={selectedTemplates.has(template.id)}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* Диалог подтверждения удаления */}
|
||
{templatesToDelete.length > 0 && (
|
||
<Dialog
|
||
open={templatesToDelete.length > 0}
|
||
onOpenChange={() => setTemplatesToDelete([])}
|
||
>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>Подтверждение удаления</DialogTitle>
|
||
<DialogDescription>
|
||
Вы действительно хотите удалить выбранные шаблоны? Это действие
|
||
нельзя отменить.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-2">
|
||
{templatesToDelete.map(template => (
|
||
<div key={template.id} className="text-sm text-gray-600">
|
||
• {template.name}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setTemplatesToDelete([])}
|
||
>
|
||
Отменить
|
||
</Button>
|
||
<Button variant="destructive" onClick={handleDeleteTemplates}>
|
||
Удалить
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|