ручной рефакторинг

This commit is contained in:
2025-07-20 08:11:27 +03:00
parent 339bb52840
commit 1d92316db7
33 changed files with 3089 additions and 6378 deletions

View File

@@ -1,15 +0,0 @@
module.exports = {
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": "warn",
"no-undef": "off",
},
};

66
.eslintrc.js Normal file
View File

@@ -0,0 +1,66 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'airbnb',
'airbnb/hooks',
'airbnb-typescript',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
},
plugins: ['prettier'],
rules: {
'consistent-return': 'off',
'import/prefer-default-export': 'off',
'prettier/prettier': 'warn',
'react/react-in-jsx-scope': 'off',
'no-param-reassign': 'off',
'react/require-default-props': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'import/extensions': 'off',
'react/jsx-props-no-spreading': 'off',
'import/order': [
'error',
{
pathGroups: [
{ pattern: 'react', group: 'builtin' },
{ pattern: '~shared/**', group: 'internal', position: 'before' },
{ pattern: '~entities/**', group: 'internal', position: 'before' },
{ pattern: '~features/**', group: 'internal', position: 'before' },
{ pattern: '~widgets/**', group: 'internal', position: 'before' },
{ pattern: '~pages/**', group: 'internal', position: 'before' },
],
pathGroupsExcludedImportTypes: ['builtin'],
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
],
'newlines-between': 'never',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
},
overrides: [
{
files: [
'src/shared/lib/test/**/*.{js,ts,jsx,tsx}',
'cypress.config.ts',
'cypress/**',
],
rules: {
'import/no-extraneous-dependencies': ['off'],
},
},
],
ignorePatterns: ['.eslintrc.js'],
}

View File

@@ -1,9 +0,0 @@
{
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"bracketSpacing": true,
"bracketSameLine": false
}

View File

@@ -1,63 +0,0 @@
# TypeScript React Tailwind Vite Template
This template provides a starting point for creating React projects with TypeScript, Tailwind CSS, and Vite.
## Overview
This template allows you to quickly set up a React project with TypeScript, Tailwind CSS, and Vite. It provides a modern development environment with features such as hot module replacement and fast refresh, making it easy to develop and customize your React applications.
## Features
- TypeScript: Develop with confidence using static type-checking and improved tooling.
- React: Build dynamic user interfaces with the popular React library.
- Tailwind CSS: Easily style and customize your components using the utility-first approach of Tailwind CSS.
- Vite: Enjoy fast development and instant hot module replacement with Vite's lightning-fast dev server.
## Installation
To create a new project using this template, follow these steps:
1. Ensure that you have Node.js installed on your machine.
2. Open a terminal and navigate to the directory where you want to create your project.
3. Run the following command to create a new project using the template:
```bash
npx create-vite@latest --template typescript-react-tailwind-vite my-project
```
Replace my-project with the desired name for your project.
4. Once the command completes, navigate into the project directory:
```bash
cd my-project
```
5. Install the dependencies:
```bash
npm install
```
## Usage
To start the development server and run the project, use the following command:
```bash
npm run dev
```
This will start the development server and open your project in the browser. Any changes you make to the source code will be automatically reflected in the browser.
## Building for Production
To build the project for production, use the following command:
```bash
npm run build
```
This will create an optimized build of your project in the dist directory.

View File

@@ -2,12 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TS + React + Tailwind</title> <title>Protoc создание протоколов</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script> <script type="module" src="/src/app/index.tsx"></script>
</body> </body>
</html> </html>

4710
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,8 @@
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^18.2.0", "react": "^18.2.0",
@@ -46,21 +48,22 @@
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0", "@typescript-eslint/parser": "^5.0.1",
"@vitejs/plugin-react-swc": "^3.0.0", "@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^3.1.7", "daisyui": "^3.1.7",
"eslint": "^8.38.0", "eslint": "^8.7.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-react-refresh": "^0.3.4",
"path": "^0.12.7", "path": "^0.12.7",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"prettier": "2.8.8", "prettier": "^3.0.0",
"prettier-plugin-tailwindcss": "^0.4.1", "prettier-eslint": "^16.1.2",
"prettier-plugin-tailwindcss": "^0.5.0",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.0.2", "typescript": "^4.4.4",
"vite": "^4.3.9" "vite": "^4.3.9"
} }
} }

View File

@@ -1,3 +1,11 @@
module.exports = { export default {
plugins: [require("prettier-plugin-tailwindcss")], plugins: ['prettier-plugin-tailwindcss'],
}; semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
printWidth: 80,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
}

View File

@@ -1,5 +1,7 @@
import { FC } from 'react' import { FC } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { TemplateProvider } from '../contexts/TemplateContext' import { TemplateProvider } from '../contexts/TemplateContext'
import { ElementsCreation } from '../pages/ElementsCreation' import { ElementsCreation } from '../pages/ElementsCreation'
import { ProtocolCreation } from '../pages/ProtocolCreation' import { ProtocolCreation } from '../pages/ProtocolCreation'

21
src/app/index.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { store } from './store'
import { initializeElementRegistry } from '../lib/element-registry-init'
import App from './App'
import './index.css'
// Инициализируем реестр элементов
initializeElementRegistry()
createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
)

View File

@@ -1,19 +0,0 @@
import { store } from "@/app/store";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { initializeElementRegistry } from '../lib/element-registry-init';
import App from "./App.tsx";
import "./index.css";
// Инициализируем реестр элементов
initializeElementRegistry();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
);

View File

@@ -1,3 +1,18 @@
import {
getAvailableElementTypes,
getElementDefinition,
} from '@/lib/element-registry'
import {
autoArrangeElements,
findFreePosition,
generateDefaultLayout,
} from '@/lib/utils'
import {
CellTarget,
ElementType,
FormLayoutSettings,
TemplateElement,
} from '@/types/template'
import { import {
Calendar, Calendar,
Droplets, Droplets,
@@ -12,21 +27,6 @@ import {
Zap, Zap,
} from 'lucide-react' } from 'lucide-react'
import React, { useState } from 'react' import React, { useState } from 'react'
import {
getAvailableElementTypes,
getElementDefinition,
} from '../../lib/element-registry'
import {
autoArrangeElements,
findFreePosition,
generateDefaultLayout,
} from '../../lib/utils'
import {
CellTarget,
ElementType,
FormLayoutSettings,
TemplateElement,
} from '../../types/template'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import { Checkbox } from '../ui/checkbox' import { Checkbox } from '../ui/checkbox'
import { import {

View File

@@ -0,0 +1,74 @@
import { FileText, Settings, Wrench } from 'lucide-react'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { Template } from '../../types/template'
import { Button } from '../ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
interface TemplateCardProps {
template: Template
onDelete: (template: Template) => void
isSelected?: boolean
}
const TemplateCard: React.FC<TemplateCardProps> = ({
template,
onDelete,
isSelected = false,
}) => {
const navigate = useNavigate()
return (
<Card className={isSelected ? 'border-2 border-blue-500' : ''}>
<CardHeader className="pb-4">
<CardTitle className="text-base font-medium">{template.name}</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{template.description && (
<p className="mb-3 line-clamp-2 text-sm text-muted-foreground">
{template.description}
</p>
)}
<div className="mb-3 flex justify-between text-xs text-muted-foreground">
<span>
Создан: {new Date(template.createdAt).toLocaleDateString()}
</span>
<span>
Обновлён: {new Date(template.updatedAt).toLocaleDateString()}
</span>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
className="flex-1"
disabled={!template.excelFile || template.elements.length === 0}
onClick={() => navigate(`/templates/${template.id}/protocols`)}
>
<FileText className="mr-2 h-3 w-3" />
{template.excelFile && template.elements.length > 0
? 'Создать протокол'
: 'Требует настройки'}
</Button>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
onClick={() => navigate(`/templates/${template.id}/edit`)}
>
<Settings className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0"
onClick={() => navigate(`/templates/${template.id}/elements`)}
>
<Wrench className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
)
}
export default TemplateCard

View File

@@ -1,16 +1,9 @@
import { FileText, Plus, Settings, Trash2, Wrench } from 'lucide-react' import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useTemplateContext } from '../../contexts/TemplateContext' import { useTemplateContext } from '../../contexts/TemplateContext'
import { Template } from '../../types/template' import { Template } from '../../types/template'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../ui/card'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -20,6 +13,7 @@ import {
DialogTrigger, DialogTrigger,
} from '../ui/dialog' } from '../ui/dialog'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import TemplateCard from './TemplateCard'
import { TemplateEditor } from './TemplateEditor' import { TemplateEditor } from './TemplateEditor'
interface TemplateManagerProps { interface TemplateManagerProps {
@@ -34,20 +28,22 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
const { templates, addTemplate, updateTemplate, deleteTemplate } = const { templates, addTemplate, updateTemplate, deleteTemplate } =
useTemplateContext() useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>( const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null, null
) )
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [isEditMode, setIsEditMode] = useState(false) const [isEditMode, setIsEditMode] = useState(false)
const [newTemplateName, setNewTemplateName] = useState('') const [newTemplateName, setNewTemplateName] = useState('')
const [newTemplateDescription, setNewTemplateDescription] = useState('') const [newTemplateDescription, setNewTemplateDescription] = useState('')
const [templateToDelete, setTemplateToDelete] = useState<Template | null>( const [isSelectionMode, setIsSelectionMode] = useState(false)
null, const [selectedTemplates, setSelectedTemplates] = useState<Set<string>>(
new Set()
) )
const [templatesToDelete, setTemplatesToDelete] = useState<Template[]>([])
// Автоматически открываем редактирование шаблона, если передан templateId // Автоматически открываем редактирование шаблона, если передан templateId
useEffect(() => { useEffect(() => {
if (templateId) { if (templateId) {
const template = templates.find((t) => t.id === templateId) const template = templates.find(t => t.id === templateId)
if (template) { if (template) {
setSelectedTemplate(template) setSelectedTemplate(template)
setIsEditMode(true) setIsEditMode(true)
@@ -80,9 +76,40 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
setSelectedTemplate(updatedTemplate) setSelectedTemplate(updatedTemplate)
} }
const handleDeleteTemplate = (template: Template) => { const handleDeleteTemplates = () => {
deleteTemplate(template.id) templatesToDelete.forEach(template => {
setTemplateToDelete(null) deleteTemplate(template.id)
})
setTemplatesToDelete([])
setIsSelectionMode(false)
setSelectedTemplates(new Set())
}
const toggleTemplateSelection = (templateId: string) => {
const newSelected = new Set(selectedTemplates)
if (newSelected.has(templateId)) {
newSelected.delete(templateId)
} else {
newSelected.add(templateId)
}
setSelectedTemplates(newSelected)
}
const handleSelectionModeToggle = () => {
if (isSelectionMode) {
setIsSelectionMode(false)
setSelectedTemplates(new Set())
setTemplatesToDelete([])
} else {
setIsSelectionMode(true)
}
}
const handleDeleteSelected = () => {
const selectedTemplatesList = templates.filter(t =>
selectedTemplates.has(t.id)
)
setTemplatesToDelete(selectedTemplatesList)
} }
if (selectedTemplate && isEditMode) { if (selectedTemplate && isEditMode) {
@@ -108,55 +135,88 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
</p> </p>
</div> </div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> <div className="flex gap-2">
<DialogTrigger asChild> {isSelectionMode && selectedTemplates.size > 0 && (
<Button className="flex items-center gap-2"> <Button
<Plus className="h-4 w-4" /> variant="destructive"
Создать onClick={handleDeleteSelected}
className="flex items-center gap-2"
>
<Trash2 className="h-4 w-4" />
Удалить ({selectedTemplates.size})
</Button> </Button>
</DialogTrigger> )}
<Button
<DialogContent className="sm:max-w-[425px]"> variant="ghost"
<DialogHeader> onClick={handleSelectionModeToggle}
<DialogTitle>Создать новый шаблон</DialogTitle> className={`flex items-center gap-2 ${
<DialogDescription> isSelectionMode
Введите название и описание для нового шаблона. ? 'bg-accent text-accent-foreground'
</DialogDescription> : 'hover:bg-accent hover:text-accent-foreground'
</DialogHeader> }`}
<div className="space-y-4 py-4"> >
<div className="space-y-2"> {isSelectionMode ? (
<label className="text-sm font-medium">Название шаблона</label> <CircleCheck className="h-4 w-4" />
<Input ) : (
placeholder="Введите название шаблона" <Circle className="h-4 w-4" />
value={newTemplateName} )}
onChange={(e) => setNewTemplateName(e.target.value)} {isSelectionMode ? 'Отменить' : 'Выбрать'}
/> </Button>
</div> <Dialog
<div className="space-y-2"> open={isCreateDialogOpen}
<label className="text-sm font-medium">Описание</label> onOpenChange={setIsCreateDialogOpen}
<Input >
placeholder="Введите описание шаблона" <DialogTrigger asChild>
value={newTemplateDescription} <Button className="flex items-center gap-2">
onChange={(e) => setNewTemplateDescription(e.target.value)} <Plus className="h-4 w-4" />
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Отмена
</Button>
<Button
onClick={handleCreateTemplate}
disabled={!newTemplateName.trim()}
>
Создать Создать
</Button> </Button>
</div> </DialogTrigger>
</DialogContent>
</Dialog> <DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Создать новый шаблон</DialogTitle>
<DialogDescription>
Введите название и описание для нового шаблона.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">
Название шаблона
</label>
<Input
placeholder="Введите название шаблона"
value={newTemplateName}
onChange={e => setNewTemplateName(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Описание</label>
<Input
placeholder="Введите описание шаблона"
value={newTemplateDescription}
onChange={e => setNewTemplateDescription(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Отмена
</Button>
<Button
onClick={handleCreateTemplate}
disabled={!newTemplateName.trim()}
>
Создать
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div> </div>
{templates.length === 0 ? ( {templates.length === 0 ? (
@@ -174,138 +234,59 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => { {templates.map(template => (
return ( <div key={template.id} className="relative">
<Card <div
key={template.id} className="cursor-pointer transition-all duration-200"
className="overflow-hidden transition-shadow hover:shadow-md" onClick={() => {
if (isSelectionMode) {
toggleTemplateSelection(template.id)
}
}}
> >
<CardHeader className="pb-3"> <TemplateCard
<div className="flex items-start justify-between"> template={template}
<div className="flex-1 space-y-1"> onDelete={() => {}} // Убрали onDelete из карточки
<CardTitle className="text-lg leading-tight"> isSelected={
{template.name} isSelectionMode && selectedTemplates.has(template.id)
</CardTitle> }
{template.description && ( />
<CardDescription className="text-sm"> </div>
{template.description} </div>
</CardDescription> ))}
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
onClick={(e) => {
e.stopPropagation()
setTemplateToDelete(template)
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center justify-between pt-2">
<div className="text-xs text-muted-foreground">
Создан: {template.createdAt.toLocaleDateString('ru-RU')}
</div>
<div className="text-xs text-muted-foreground">
Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="mb-4 flex items-center gap-1 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<FileText className="h-3 w-3" />
<span>{template.excelFile ? '✓' : '✗'}</span>
</div>
<div className="flex items-center gap-1">
<Wrench className="h-3 w-3" />
<span>{template.elements.length}</span>
</div>
</div>
<div className="space-y-2">
<Button
size="sm"
className="w-full"
onClick={(e) => {
e.stopPropagation()
console.log('Navigating to protocols:', template.id)
navigate(`/templates/${template.id}/protocols`)
}}
disabled={
!template.excelFile || template.elements.length === 0
}
>
<FileText className="mr-2 h-4 w-4" />
{template.excelFile && template.elements.length > 0
? 'Создать протокол'
: 'Требует настройки'}
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={(e) => {
e.stopPropagation()
console.log(
'Navigating to template edit:',
template.id,
)
navigate(`/templates/${template.id}/edit`)
}}
>
<Settings className="mr-1 h-3 w-3" />
Шаблон
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={(e) => {
e.stopPropagation()
navigate(`/templates/${template.id}/elements`)
}}
>
<Wrench className="mr-1 h-3 w-3" />
Элементы
</Button>
</div>
</div>
</CardContent>
</Card>
)
})}
</div> </div>
)} )}
{/* Диалог подтверждения удаления */} {/* Диалог подтверждения удаления */}
<Dialog <Dialog
open={!!templateToDelete} open={templatesToDelete.length > 0}
onOpenChange={() => setTemplateToDelete(null)} onOpenChange={() => setTemplatesToDelete([])}
> >
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Удалить шаблон</DialogTitle> <DialogTitle>Удалить шаблоны</DialogTitle>
<DialogDescription> <DialogDescription>
Вы уверены, что хотите удалить шаблон "{templateToDelete?.name}"? Вы уверены, что хотите удалить {templatesToDelete.length}{' '}
Это действие нельзя будет отменить. шаблонов? Это действие нельзя будет отменить.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-2">
{templatesToDelete.map(template => (
<div
key={template.id}
className="flex items-center gap-2 rounded border p-2"
>
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{template.name}</span>
</div>
))}
</div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setTemplateToDelete(null)}> <Button variant="outline" onClick={() => setTemplatesToDelete([])}>
Отмена Отмена
</Button> </Button>
<Button <Button variant="destructive" onClick={handleDeleteTemplates}>
variant="destructive"
onClick={() =>
templateToDelete && handleDeleteTemplate(templateToDelete)
}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Удалить Удалить
</Button> </Button>

View File

@@ -0,0 +1,175 @@
import { ElementConstructor } from '@/components/TemplateManager/ElementConstructor'
import { Button } from '@/components/ui/button'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { FormLayoutSettings, Template, TemplateElement } from '@/types/template'
import { ArrowLeft, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
export const ElementsCreation: FC = () => {
const navigate = useNavigate()
const { templateId } = useParams<{ templateId: string }>()
const { templates, updateTemplate } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null
)
// Автоматически выбираем шаблон, если передан templateId
useEffect(() => {
if (templateId) {
const template = templates.find(t => t.id === templateId)
if (template) {
setSelectedTemplate(template)
} else {
// Если шаблон не найден, перенаправляем на страницу со списком шаблонов
navigate('/templates', { replace: true })
}
}
}, [templateId, templates, navigate])
const handleElementAdd = (element: TemplateElement) => {
console.log('handleElementAdd called with element:', element)
if (!selectedTemplate) {
console.log('No selected template')
return
}
const updatedTemplate = {
...selectedTemplate,
elements: [...selectedTemplate.elements, element],
updatedAt: new Date(),
}
console.log('Updated template:', updatedTemplate)
console.log('Elements count:', updatedTemplate.elements.length)
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementUpdate = (
elementId: string,
updates: Partial<TemplateElement>
) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
elements: selectedTemplate.elements.map(el =>
el.id === elementId ? { ...el, ...updates } : el
),
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementDelete = (elementId: string) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
elements: selectedTemplate.elements.filter(el => el.id !== elementId),
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementsReorder = (reorderedElements: TemplateElement[]) => {
if (!selectedTemplate) return
// Обновляем порядок элементов
const elementsWithOrder = reorderedElements.map((element, index) => ({
...element,
order: index,
}))
const updatedTemplate = {
...selectedTemplate,
elements: elementsWithOrder,
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleLayoutSettingsChange = (settings: FormLayoutSettings) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
layoutSettings: settings,
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
// Настройки отображения по умолчанию
const defaultLayoutSettings: FormLayoutSettings = {
gridSize: 20,
showGrid: true,
}
if (!selectedTemplate) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<Wrench 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>
<Button onClick={() => navigate('/templates')}>
К списку шаблонов
</Button>
</div>
</div>
)
}
return (
<div className="h-full">
<div className="sticky top-0 z-50 border-b border-gray-200 bg-white p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => navigate(`/templates`)}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-semibold text-gray-800">
Конструктор элементов: {selectedTemplate.name}
</h1>
<p className="mt-1 text-sm text-gray-600">
Создайте элементы формы и разместите их на холсте
перетаскиванием
</p>
</div>
</div>
</div>
</div>
<div className="h-[calc(100vh-104px)]">
<ElementConstructor
elements={selectedTemplate.elements}
layoutSettings={
selectedTemplate.layoutSettings || defaultLayoutSettings
}
onElementAdd={handleElementAdd}
onElementUpdate={handleElementUpdate}
onElementDelete={handleElementDelete}
onElementsReorder={handleElementsReorder}
onLayoutSettingsChange={handleLayoutSettingsChange}
/>
</div>
</div>
)
}

View File

@@ -1,3 +1,2 @@
import { ElementsCreation } from "./ui/Page"; export { ElementsCreation } from './Page';
export { ElementsCreation };

View File

@@ -1,250 +0,0 @@
import { Plus, Wrench } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor'
import { Button } from '../../../../components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../../../../components/ui/card'
import { useTemplateContext } from '../../../../contexts/TemplateContext'
import {
FormLayoutSettings,
Template,
TemplateElement,
} from '../../../../types/template'
export const ElementsCreation: FC = () => {
const navigate = useNavigate()
const { templateId } = useParams<{ templateId: string }>()
const { templates, updateTemplate } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null,
)
// Автоматически выбираем шаблон, если передан templateId
useEffect(() => {
if (templateId) {
const template = templates.find((t) => t.id === templateId)
if (template) {
setSelectedTemplate(template)
} else {
// Если шаблон не найден, перенаправляем на страницу со списком шаблонов
navigate('/templates', { replace: true })
}
}
}, [templateId, templates, navigate])
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template)
}
const handleElementAdd = (element: TemplateElement) => {
console.log('handleElementAdd called with element:', element)
if (!selectedTemplate) {
console.log('No selected template')
return
}
const updatedTemplate = {
...selectedTemplate,
elements: [...selectedTemplate.elements, element],
updatedAt: new Date(),
}
console.log('Updated template:', updatedTemplate)
console.log('Elements count:', updatedTemplate.elements.length)
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementUpdate = (
elementId: string,
updates: Partial<TemplateElement>,
) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
elements: selectedTemplate.elements.map((el) =>
el.id === elementId ? { ...el, ...updates } : el,
),
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementDelete = (elementId: string) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
elements: selectedTemplate.elements.filter((el) => el.id !== elementId),
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleElementsReorder = (reorderedElements: TemplateElement[]) => {
if (!selectedTemplate) return
// Обновляем порядок элементов
const elementsWithOrder = reorderedElements.map((element, index) => ({
...element,
order: index,
}))
const updatedTemplate = {
...selectedTemplate,
elements: elementsWithOrder,
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
const handleLayoutSettingsChange = (settings: FormLayoutSettings) => {
if (!selectedTemplate) return
const updatedTemplate = {
...selectedTemplate,
layoutSettings: settings,
updatedAt: new Date(),
}
updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate)
}
// Настройки отображения по умолчанию
const defaultLayoutSettings: FormLayoutSettings = {
gridSize: 20,
showGrid: true,
}
if (selectedTemplate) {
return (
<div className="h-full">
<div className="sticky top-0 z-50 border-b border-gray-200 bg-white p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => navigate(`/templates`)}>
Назад
</Button>
<div>
<h1 className="text-xl font-semibold text-gray-800">
Конструктор элементов: {selectedTemplate.name}
</h1>
<p className="mt-1 text-sm text-gray-600">
Создайте элементы формы и разместите их на холсте
перетаскиванием
</p>
</div>
</div>
</div>
</div>
<div className="h-[calc(100vh-104px)]">
<ElementConstructor
elements={selectedTemplate.elements}
layoutSettings={
selectedTemplate.layoutSettings || defaultLayoutSettings
}
onElementAdd={handleElementAdd}
onElementUpdate={handleElementUpdate}
onElementDelete={handleElementDelete}
onElementsReorder={handleElementsReorder}
onLayoutSettingsChange={handleLayoutSettingsChange}
/>
</div>
</div>
)
}
return (
<div className="mx-auto max-w-7xl p-6">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">
Создание элементов
</h1>
<p className="mt-2 text-gray-600">
Выберите шаблон для создания элементов интерфейса
</p>
</div>
<Button
variant="outline"
onClick={() => navigate('/templates')}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Создать шаблон
</Button>
</div>
{templates.length === 0 ? (
<div className="py-12 text-center">
<Wrench 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-6 text-gray-600">
Создайте шаблон протокола для начала работы с элементами
</p>
<Button onClick={() => navigate('/templates')}>
<Plus className="mr-2 h-4 w-4" />
Создать шаблон
</Button>
</div>
) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => (
<Card
key={template.id}
className="cursor-pointer transition-shadow hover:shadow-lg"
>
<CardHeader>
<CardTitle className="text-lg">{template.name}</CardTitle>
{template.description && (
<CardDescription className="mt-1">
{template.description}
</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center gap-2">
<Wrench className="h-4 w-4" />
<span>{template.elements.length} элементов создано</span>
</div>
<div className="mt-2 text-xs text-gray-500">
Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
</div>
</div>
<div className="mt-4">
<Button
className="w-full"
onClick={() => handleTemplateSelect(template)}
>
Создать элементы
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
)
}

View File

@@ -1 +0,0 @@
export { ElementsCreation } from './Page';

View File

@@ -1,3 +0,0 @@
import NoMatch from "./ui/Page/Page";
export { NoMatch };

View File

@@ -1,20 +0,0 @@
import { FC } from "react";
import { Link } from "react-router-dom";
const NoMatch: FC = () => {
return (
<section>
<div className="flex min-h-screen w-screen flex-col items-center justify-center gap-y-5">
<h1 className="bg-gradient-to-l from-primary-content via-secondary to-primary bg-clip-text text-9xl font-bold text-transparent">
404
</h1>
<p className="text-3xl font-medium text-neutral">Page not found</p>
<Link className="btn-primary-content btn px-16" to="/">
Go back
</Link>
</div>
</section>
);
};
export default NoMatch;

View File

@@ -1,43 +1,32 @@
import { import { StandardsSelector } from '@/components/StandardsElement/StandardsSelector'
ArrowLeft, import { Badge } from '@/components/ui/badge'
Download, import { Button } from '@/components/ui/button'
FileText, import { Checkbox } from '@/components/ui/checkbox'
Grid, import { Input } from '@/components/ui/input'
Plus, import { Label } from '@/components/ui/label'
Save, import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
Settings,
Wrench,
} from 'lucide-react'
import { FC, useEffect, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { StandardsSelector } from '../../../../components/StandardsElement/StandardsSelector'
import { Badge } from '../../../../components/ui/badge'
import { Button } from '../../../../components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../../../../components/ui/card'
import { Checkbox } from '../../../../components/ui/checkbox'
import { Input } from '../../../../components/ui/input'
import { Label } from '../../../../components/ui/label'
import {
RadioGroup,
RadioGroupItem,
} from '../../../../components/ui/radio-group'
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '../../../../components/ui/select' } from '@/components/ui/select'
import { Textarea } from '../../../../components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { useTemplateContext } from '../../../../contexts/TemplateContext' import { useTemplateContext } from '@/contexts/TemplateContext'
import { getElementDefinition } from '../../../../lib/element-registry' import { getElementDefinition } from '@/lib/element-registry'
import { Template, TemplateElement } from '../../../../types/template' import { Template, TemplateElement } from '@/types/template'
import {
ArrowLeft,
Download,
FileText,
Grid,
Save,
Settings,
Wrench,
} from 'lucide-react'
import { FC, useEffect, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
// Моковые данные для эталонов (такие же как в StandardsSelector) // Моковые данные для эталонов (такие же как в StandardsSelector)
const mockStandards = [ const mockStandards = [
@@ -100,7 +89,7 @@ const mockStandards = [
// Функция для получения данных эталона по ID // Функция для получения данных эталона по ID
const getStandardById = (id: string) => { const getStandardById = (id: string) => {
return mockStandards.find((standard) => standard.id === id) return mockStandards.find(standard => standard.id === id)
} }
// Функция для получения типа эталона в сокращенном виде // Функция для получения типа эталона в сокращенном виде
@@ -137,7 +126,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
<Input <Input
placeholder={element.placeholder} placeholder={element.placeholder}
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
required={element.required} required={element.required}
/> />
) )
@@ -147,7 +136,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
<Textarea <Textarea
placeholder={element.placeholder} placeholder={element.placeholder}
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
required={element.required} required={element.required}
rows={3} rows={3}
/> />
@@ -159,7 +148,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
type="number" type="number"
placeholder={element.placeholder} placeholder={element.placeholder}
value={value || ''} value={value || ''}
onChange={(e) => onChange={e =>
onChange(e.target.value ? parseFloat(e.target.value) : '') onChange(e.target.value ? parseFloat(e.target.value) : '')
} }
required={element.required} required={element.required}
@@ -171,7 +160,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
<Input <Input
type="date" type="date"
value={value || ''} value={value || ''}
onChange={(e) => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
required={element.required} required={element.required}
/> />
) )
@@ -185,7 +174,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
/> />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{element.options?.map((option) => ( {element.options?.map(option => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>
@@ -197,7 +186,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
case 'radio': case 'radio':
return ( return (
<RadioGroup value={value || ''} onValueChange={onChange}> <RadioGroup value={value || ''} onValueChange={onChange}>
{element.options?.map((option) => ( {element.options?.map(option => (
<div key={option.value} className="flex items-center space-x-2"> <div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem <RadioGroupItem
value={option.value} value={option.value}
@@ -231,7 +220,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
case 'standards': case 'standards':
const selectedStandards = Array.isArray(value) ? value : [] const selectedStandards = Array.isArray(value) ? value : []
const selectedStandardsData = selectedStandards const selectedStandardsData = selectedStandards
.map((id) => getStandardById(id)) .map(id => getStandardById(id))
.filter(Boolean) .filter(Boolean)
return ( return (
@@ -282,7 +271,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
{getStandardTypeBadge(standard.type)} {getStandardTypeBadge(standard.type)}
</Badge> </Badge>
</div> </div>
), )
)} )}
</div> </div>
) : ( ) : (
@@ -379,7 +368,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
const [formData, setFormData] = useState<Record<string, any>>({}) const [formData, setFormData] = useState<Record<string, any>>({})
const handleFieldChange = (elementId: string, value: any) => { const handleFieldChange = (elementId: string, value: any) => {
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
[elementId]: value, [elementId]: value,
})) }))
@@ -388,7 +377,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
const handleSave = () => { const handleSave = () => {
// Ищем элемент с названием протокола (должен иметь специальный тип или название) // Ищем элемент с названием протокола (должен иметь специальный тип или название)
const protocolNameElement = template.elements.find( const protocolNameElement = template.elements.find(
(el) => el.name === 'protocolName' || el.label === 'Название протокола', el => el.name === 'protocolName' || el.label === 'Название протокола'
) )
const protocolName = protocolNameElement const protocolName = protocolNameElement
? formData[protocolNameElement.id] ? formData[protocolNameElement.id]
@@ -420,8 +409,8 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
const contentHeight = Math.max( const contentHeight = Math.max(
0, 0,
...template.elements.map( ...template.elements.map(
(el) => (el.layout?.y || 0) + (el.layout?.height || 0), el => (el.layout?.y || 0) + (el.layout?.height || 0)
), )
) )
return { return {
@@ -432,7 +421,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
// Проверяем есть ли кнопка сохранения (есть ли название протокола) // Проверяем есть ли кнопка сохранения (есть ли название протокола)
const protocolNameElement = template.elements.find( const protocolNameElement = template.elements.find(
(el) => el.name === 'protocolName' || el.label === 'Название протокола', el => el.name === 'protocolName' || el.label === 'Название протокола'
) )
const canSave = protocolNameElement const canSave = protocolNameElement
? !!formData[protocolNameElement.id]?.trim() ? !!formData[protocolNameElement.id]?.trim()
@@ -473,7 +462,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
className="relative mx-auto my-8" className="relative mx-auto my-8"
style={{ width: canvasSize.width, height: canvasSize.height }} style={{ width: canvasSize.width, height: canvasSize.height }}
> >
{template.elements.map((element) => { {template.elements.map(element => {
const layout = element.layout || { const layout = element.layout || {
x: 50, x: 50,
y: 50, y: 50,
@@ -495,7 +484,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
<FormElement <FormElement
element={element} element={element}
value={formData[element.id]} value={formData[element.id]}
onChange={(value) => handleFieldChange(element.id, value)} onChange={value => handleFieldChange(element.id, value)}
/> />
</div> </div>
) )
@@ -528,25 +517,19 @@ export const ProtocolCreation: FC = () => {
const { templateId } = useParams<{ templateId?: string }>() const { templateId } = useParams<{ templateId?: string }>()
const { templates } = useTemplateContext() const { templates } = useTemplateContext()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>( const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null, null
) )
// Автоматически выбираем шаблон если передан ID в URL // Автоматически выбираем шаблон если передан ID в URL
useEffect(() => { useEffect(() => {
if (templateId && templates.length > 0) { if (templateId && templates.length > 0) {
const template = templates.find((t) => t.id === templateId) const template = templates.find(t => t.id === templateId)
if (template) { if (template) {
setSelectedTemplate(template) setSelectedTemplate(template)
} }
} }
}, [templateId, templates]) }, [templateId, templates])
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template)
// Обновляем URL с ID выбранного шаблона
navigate(`/create-protocol/${template.id}`)
}
const handleProtocolSave = (data: Record<string, any>) => { const handleProtocolSave = (data: Record<string, any>) => {
console.log('Сохранение протокола:', data) console.log('Сохранение протокола:', data)
// Здесь будет логика сохранения протокола // Здесь будет логика сохранения протокола
@@ -555,104 +538,33 @@ export const ProtocolCreation: FC = () => {
} }
const handleBack = () => { const handleBack = () => {
setSelectedTemplate(null)
// Возвращаемся к списку шаблонов
navigate('/templates') navigate('/templates')
} }
if (selectedTemplate) { if (!selectedTemplate) {
return ( return (
<ProtocolForm <div className="flex h-screen items-center justify-center">
template={selectedTemplate} <div className="text-center">
onSave={handleProtocolSave} <FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
onBack={handleBack} <h3 className="mb-2 text-lg font-medium text-gray-900">
/> Шаблон не найден
</h3>
<p className="mb-4 text-gray-600">
Перейдите к списку шаблонов для выбора
</p>
<Button onClick={() => navigate('/templates')}>
К списку шаблонов
</Button>
</div>
</div>
) )
} }
return ( return (
<div className="mx-auto max-w-7xl p-6"> <ProtocolForm
<div className="mb-6 flex items-center justify-between"> template={selectedTemplate}
<div> onSave={handleProtocolSave}
<h1 className="text-3xl font-bold text-gray-900"> onBack={handleBack}
Создание протокола />
</h1>
<p className="mt-2 text-gray-600">
Выберите шаблон для создания нового протокола
</p>
</div>
<Button
variant="outline"
onClick={() => navigate('/templates')}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Создать шаблон
</Button>
</div>
{templates.length === 0 ? (
<div className="py-12 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-6 text-gray-600">
Создайте шаблон протокола для начала работы
</p>
<Button onClick={() => navigate('/templates')}>
<Plus className="mr-2 h-4 w-4" />
Создать шаблон
</Button>
</div>
) : (
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{templates.map((template) => (
<Card
key={template.id}
className="cursor-pointer transition-shadow hover:shadow-lg"
>
<CardHeader>
<CardTitle className="text-lg">{template.name}</CardTitle>
{template.description && (
<CardDescription className="mt-1">
{template.description}
</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<span>
{template.elements.length} элементов для заполнения
</span>
</div>
<div className="mt-2 text-xs text-gray-500">
Создан: {template.createdAt.toLocaleDateString('ru-RU')}
</div>
</div>
<div className="mt-4">
<Button
className="w-full"
onClick={() => handleTemplateSelect(template)}
disabled={
!template.excelFile || template.elements.length === 0
}
>
{template.excelFile
? template.elements.length > 0
? 'Создать протокол'
: 'Нет элементов формы'
: 'Шаблон не настроен'}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
) )
} }

View File

@@ -1 +1 @@
export { ProtocolCreation } from './ui/Page/Page'; export { ProtocolCreation } from './Page';

View File

@@ -1,7 +1,7 @@
import { TemplateManager } from '@/components/TemplateManager/TemplateManager'
import { useTemplateContext } from '@/contexts/TemplateContext'
import { FC, useEffect } from 'react' import { FC, useEffect } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { TemplateManager } from '../../../../components/TemplateManager/TemplateManager'
import { useTemplateContext } from '../../../../contexts/TemplateContext'
export const TemplateSetup: FC = () => { export const TemplateSetup: FC = () => {
const { templateId } = useParams<{ templateId: string }>() const { templateId } = useParams<{ templateId: string }>()
@@ -9,7 +9,7 @@ export const TemplateSetup: FC = () => {
useEffect(() => { useEffect(() => {
if (templateId) { if (templateId) {
const template = templates.find((t) => t.id === templateId) const template = templates.find(t => t.id === templateId)
if (template) { if (template) {
console.log('Opening template for editing:', template) console.log('Opening template for editing:', template)
} }

View File

@@ -1 +1 @@
export { TemplateSetup } from './ui/Page/Page'; export { TemplateSetup } from './Page'

View File

@@ -1,6 +1,5 @@
import { ElementsCreation } from './ElementsCreation' import { ElementsCreation } from './ElementsCreation'
import { NoMatch } from './NoMatch'
import { ProtocolCreation } from './ProtocolCreation' import { ProtocolCreation } from './ProtocolCreation'
import { TemplateSetup } from './TemplateSetup' import { TemplateSetup } from './TemplateSetup'
export { ElementsCreation, NoMatch, ProtocolCreation, TemplateSetup } export { ElementsCreation, ProtocolCreation, TemplateSetup }

View File

@@ -10,7 +10,7 @@ import {
SaveFormulasResponse, SaveFormulasResponse,
SyncStatus, SyncStatus,
TemplateFormulaData, TemplateFormulaData,
} from '../types/formula-api' } from '@/types/formula-api'
/** /**
* Сервис для работы с API формул электронных таблиц * Сервис для работы с API формул электронных таблиц
@@ -31,7 +31,7 @@ class FormulaApiService {
* Сохранение формул на сервер * Сохранение формул на сервер
*/ */
async saveFormulas( async saveFormulas(
request: SaveFormulasRequest, request: SaveFormulasRequest
): Promise<SaveFormulasResponse> { ): Promise<SaveFormulasResponse> {
this.emitEvent({ type: 'SAVE_START', templateId: request.templateId }) this.emitEvent({ type: 'SAVE_START', templateId: request.templateId })
@@ -123,7 +123,7 @@ class FormulaApiService {
* Загрузка сохраненных формул * Загрузка сохраненных формул
*/ */
async loadFormulas( async loadFormulas(
request: LoadFormulasRequest, request: LoadFormulasRequest
): Promise<LoadFormulasResponse> { ): Promise<LoadFormulasResponse> {
this.emitEvent({ type: 'LOAD_START', templateId: request.templateId }) this.emitEvent({ type: 'LOAD_START', templateId: request.templateId })
@@ -205,7 +205,7 @@ class FormulaApiService {
* Получение истории изменений формул * Получение истории изменений формул
*/ */
async getFormulaHistory( async getFormulaHistory(
request: FormulaHistoryRequest, request: FormulaHistoryRequest
): Promise<FormulaHistoryResponse> { ): Promise<FormulaHistoryResponse> {
try { try {
// Имитация задержки API // Имитация задержки API
@@ -306,7 +306,7 @@ class FormulaApiService {
*/ */
addEventListener( addEventListener(
eventType: string, eventType: string,
listener: (event: FormulaApiEvent) => void, listener: (event: FormulaApiEvent) => void
): void { ): void {
if (!this.eventListeners.has(eventType)) { if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []) this.eventListeners.set(eventType, [])
@@ -319,7 +319,7 @@ class FormulaApiService {
*/ */
removeEventListener( removeEventListener(
eventType: string, eventType: string,
listener: (event: FormulaApiEvent) => void, listener: (event: FormulaApiEvent) => void
): void { ): void {
const listeners = this.eventListeners.get(eventType) const listeners = this.eventListeners.get(eventType)
if (listeners) { if (listeners) {
@@ -335,7 +335,7 @@ class FormulaApiService {
*/ */
private emitEvent(event: FormulaApiEvent): void { private emitEvent(event: FormulaApiEvent): void {
const listeners = this.eventListeners.get(event.type) || [] const listeners = this.eventListeners.get(event.type) || []
listeners.forEach((listener) => { listeners.forEach(listener => {
try { try {
listener(event) listener(event)
} catch (error) { } catch (error) {
@@ -349,7 +349,7 @@ class FormulaApiService {
*/ */
private async delay(min: number, max: number): Promise<void> { private async delay(min: number, max: number): Promise<void> {
const delay = min + Math.random() * (max - min) const delay = min + Math.random() * (max - min)
await new Promise((resolve) => setTimeout(resolve, delay)) await new Promise(resolve => setTimeout(resolve, delay))
} }
/** /**

25
src/types/imports.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
// Типы для контроля импортов между слоями архитектуры
declare module 'app/*' {
const content: any
export default content
}
declare module 'pages/*' {
const content: any
export default content
}
declare module 'features/*' {
const content: any
export default content
}
declare module 'entities/*' {
const content: any
export default content
}
declare module 'shared/*' {
const content: any
export default content
}

1
src/types/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -1,38 +1,25 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "noImplicitAny": true,
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "target": "ESNext",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"allowJs": true,
/* Linting */ "moduleResolution": "node",
"strict": true, "esModuleInterop": true,
"noUnusedLocals": true, "allowSyntheticDefaultImports": true,
"noUnusedParameters": true, "resolveJsonModule": true,
"noFallthroughCasesInSwitch": true, "strictNullChecks": true,
"outDir": "./dist/",
/* Path */ "baseUrl": ".",
"baseUrl": "./",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["./src/*"],
"shared/*": ["src/shared/*"],
"entities/*": ["src/entities/*"],
"features/*": ["src/features/*"],
"widgets/*": ["src/components/*"],
"pages/*": ["src/pages/*"],
"processes/*": ["src/processes/*"],
"app/*": ["src/app/*"]
} }
}, },
"include": ["src", "types/vite-env.d.ts"], "ts-node": {
"references": [{ "path": "./tsconfig.node.json" }] "compilerOptions": {
} "module": "CommonJS"
}
},
"exclude": ["node_modules", "dist", "build", "coverage"]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

1
types/vite-env.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,6 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path"; import path from "path";
import { defineConfig } from "vite";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

3309
yarn.lock

File diff suppressed because it is too large Load Diff