ручной рефакторинг
This commit is contained in:
@@ -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
66
.eslintrc.js
Normal 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'],
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"bracketSameLine": false
|
|
||||||
}
|
|
||||||
63
README.md
63
README.md
@@ -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.
|
|
||||||
@@ -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
4710
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
21
src/app/index.tsx
Normal 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>,
|
||||||
|
)
|
||||||
@@ -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>,
|
|
||||||
);
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
74
src/components/TemplateManager/TemplateCard.tsx
Normal file
74
src/components/TemplateManager/TemplateCard.tsx
Normal 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
|
||||||
@@ -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 = () => {
|
||||||
|
templatesToDelete.forEach(template => {
|
||||||
deleteTemplate(template.id)
|
deleteTemplate(template.id)
|
||||||
setTemplateToDelete(null)
|
})
|
||||||
|
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,7 +135,37 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<div className="flex gap-2">
|
||||||
|
{isSelectionMode && selectedTemplates.size > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Удалить ({selectedTemplates.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleSelectionModeToggle}
|
||||||
|
className={`flex items-center gap-2 ${
|
||||||
|
isSelectionMode
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: 'hover:bg-accent hover:text-accent-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSelectionMode ? (
|
||||||
|
<CircleCheck className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Circle className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isSelectionMode ? 'Отменить' : 'Выбрать'}
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
open={isCreateDialogOpen}
|
||||||
|
onOpenChange={setIsCreateDialogOpen}
|
||||||
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="flex items-center gap-2">
|
<Button className="flex items-center gap-2">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
@@ -125,11 +182,13 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Название шаблона</label>
|
<label className="text-sm font-medium">
|
||||||
|
Название шаблона
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Введите название шаблона"
|
placeholder="Введите название шаблона"
|
||||||
value={newTemplateName}
|
value={newTemplateName}
|
||||||
onChange={(e) => setNewTemplateName(e.target.value)}
|
onChange={e => setNewTemplateName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -137,7 +196,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Введите описание шаблона"
|
placeholder="Введите описание шаблона"
|
||||||
value={newTemplateDescription}
|
value={newTemplateDescription}
|
||||||
onChange={(e) => setNewTemplateDescription(e.target.value)}
|
onChange={e => setNewTemplateDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,6 +217,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
@@ -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) {
|
||||||
<CardHeader className="pb-3">
|
toggleTemplateSelection(template.id)
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1 space-y-1">
|
|
||||||
<CardTitle className="text-lg leading-tight">
|
|
||||||
{template.name}
|
|
||||||
</CardTitle>
|
|
||||||
{template.description && (
|
|
||||||
<CardDescription className="text-sm">
|
|
||||||
{template.description}
|
|
||||||
</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" />
|
<TemplateCard
|
||||||
Шаблон
|
template={template}
|
||||||
</Button>
|
onDelete={() => {}} // Убрали onDelete из карточки
|
||||||
<Button
|
isSelected={
|
||||||
variant="outline"
|
isSelectionMode && selectedTemplates.has(template.id)
|
||||||
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>
|
||||||
</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>
|
||||||
|
|||||||
175
src/pages/ElementsCreation/Page.tsx
Normal file
175
src/pages/ElementsCreation/Page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
import { ElementsCreation } from "./ui/Page";
|
export { ElementsCreation } from './Page';
|
||||||
|
|
||||||
export { ElementsCreation };
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { ElementsCreation } from './Page';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import NoMatch from "./ui/Page/Page";
|
|
||||||
|
|
||||||
export { NoMatch };
|
|
||||||
@@ -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;
|
|
||||||
@@ -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,12 +538,28 @@ export const ProtocolCreation: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setSelectedTemplate(null)
|
|
||||||
// Возвращаемся к списку шаблонов
|
|
||||||
navigate('/templates')
|
navigate('/templates')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTemplate) {
|
if (!selectedTemplate) {
|
||||||
|
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-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 (
|
return (
|
||||||
<ProtocolForm
|
<ProtocolForm
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
@@ -569,90 +568,3 @@ export const ProtocolCreation: FC = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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">
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
export { ProtocolCreation } from './ui/Page/Page';
|
export { ProtocolCreation } from './Page';
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export { TemplateSetup } from './ui/Page/Page';
|
export { TemplateSetup } from './Page'
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
25
src/types/imports.d.ts
vendored
Normal 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
1
src/types/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
@@ -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
1
types/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user