ручной рефакторинг
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">
|
||||
<head>
|
||||
<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" />
|
||||
<title>TS + React + Tailwind</title>
|
||||
<title>Protoc создание протоколов</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app/main.tsx"></script>
|
||||
<script type="module" src="/src/app/index.tsx"></script>
|
||||
</body>
|
||||
</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",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"immer": "^10.1.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -46,21 +48,22 @@
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@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",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^3.1.7",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"path": "^0.12.7",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-eslint": "^16.1.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.0",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"typescript": "^4.4.4",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
module.exports = {
|
||||
plugins: [require("prettier-plugin-tailwindcss")],
|
||||
};
|
||||
export default {
|
||||
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 { Navigate, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { TemplateProvider } from '../contexts/TemplateContext'
|
||||
import { ElementsCreation } from '../pages/ElementsCreation'
|
||||
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 {
|
||||
Calendar,
|
||||
Droplets,
|
||||
@@ -12,21 +27,6 @@ import {
|
||||
Zap,
|
||||
} from 'lucide-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 { Checkbox } from '../ui/checkbox'
|
||||
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 { useNavigate } from 'react-router-dom'
|
||||
import { useTemplateContext } from '../../contexts/TemplateContext'
|
||||
import { Template } from '../../types/template'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '../ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -20,6 +13,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import TemplateCard from './TemplateCard'
|
||||
import { TemplateEditor } from './TemplateEditor'
|
||||
|
||||
interface TemplateManagerProps {
|
||||
@@ -34,20 +28,22 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
const { templates, addTemplate, updateTemplate, deleteTemplate } =
|
||||
useTemplateContext()
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null,
|
||||
null
|
||||
)
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
const [newTemplateName, setNewTemplateName] = useState('')
|
||||
const [newTemplateDescription, setNewTemplateDescription] = useState('')
|
||||
const [templateToDelete, setTemplateToDelete] = useState<Template | null>(
|
||||
null,
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false)
|
||||
const [selectedTemplates, setSelectedTemplates] = useState<Set<string>>(
|
||||
new Set()
|
||||
)
|
||||
const [templatesToDelete, setTemplatesToDelete] = useState<Template[]>([])
|
||||
|
||||
// Автоматически открываем редактирование шаблона, если передан templateId
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
const template = templates.find((t) => t.id === templateId)
|
||||
const template = templates.find(t => t.id === templateId)
|
||||
if (template) {
|
||||
setSelectedTemplate(template)
|
||||
setIsEditMode(true)
|
||||
@@ -80,9 +76,40 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
setSelectedTemplate(updatedTemplate)
|
||||
}
|
||||
|
||||
const handleDeleteTemplate = (template: Template) => {
|
||||
const handleDeleteTemplates = () => {
|
||||
templatesToDelete.forEach(template => {
|
||||
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) {
|
||||
@@ -108,7 +135,37 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
</p>
|
||||
</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>
|
||||
<Button className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -125,11 +182,13 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Название шаблона</label>
|
||||
<label className="text-sm font-medium">
|
||||
Название шаблона
|
||||
</label>
|
||||
<Input
|
||||
placeholder="Введите название шаблона"
|
||||
value={newTemplateName}
|
||||
onChange={(e) => setNewTemplateName(e.target.value)}
|
||||
onChange={e => setNewTemplateName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -137,7 +196,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
<Input
|
||||
placeholder="Введите описание шаблона"
|
||||
value={newTemplateDescription}
|
||||
onChange={(e) => setNewTemplateDescription(e.target.value)}
|
||||
onChange={e => setNewTemplateDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,6 +217,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{templates.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
@@ -174,138 +234,59 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{templates.map((template) => {
|
||||
return (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="overflow-hidden transition-shadow hover:shadow-md"
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<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
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{templates.map(template => (
|
||||
<div key={template.id} className="relative">
|
||||
<div
|
||||
className="cursor-pointer transition-all duration-200"
|
||||
onClick={() => {
|
||||
if (isSelectionMode) {
|
||||
toggleTemplateSelection(template.id)
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<TemplateCard
|
||||
template={template}
|
||||
onDelete={() => {}} // Убрали onDelete из карточки
|
||||
isSelected={
|
||||
isSelectionMode && selectedTemplates.has(template.id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Диалог подтверждения удаления */}
|
||||
<Dialog
|
||||
open={!!templateToDelete}
|
||||
onOpenChange={() => setTemplateToDelete(null)}
|
||||
open={templatesToDelete.length > 0}
|
||||
onOpenChange={() => setTemplatesToDelete([])}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Удалить шаблон</DialogTitle>
|
||||
<DialogTitle>Удалить шаблоны</DialogTitle>
|
||||
<DialogDescription>
|
||||
Вы уверены, что хотите удалить шаблон "{templateToDelete?.name}"?
|
||||
Это действие нельзя будет отменить.
|
||||
Вы уверены, что хотите удалить {templatesToDelete.length}{' '}
|
||||
шаблонов? Это действие нельзя будет отменить.
|
||||
</DialogDescription>
|
||||
</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">
|
||||
<Button variant="outline" onClick={() => setTemplateToDelete(null)}>
|
||||
<Button variant="outline" onClick={() => setTemplatesToDelete([])}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
templateToDelete && handleDeleteTemplate(templateToDelete)
|
||||
}
|
||||
>
|
||||
<Button variant="destructive" onClick={handleDeleteTemplates}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Удалить
|
||||
</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 {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
FileText,
|
||||
Grid,
|
||||
Plus,
|
||||
Save,
|
||||
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 { StandardsSelector } from '@/components/StandardsElement/StandardsSelector'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../../../../components/ui/select'
|
||||
import { Textarea } from '../../../../components/ui/textarea'
|
||||
import { useTemplateContext } from '../../../../contexts/TemplateContext'
|
||||
import { getElementDefinition } from '../../../../lib/element-registry'
|
||||
import { Template, TemplateElement } from '../../../../types/template'
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { useTemplateContext } from '@/contexts/TemplateContext'
|
||||
import { getElementDefinition } from '@/lib/element-registry'
|
||||
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)
|
||||
const mockStandards = [
|
||||
@@ -100,7 +89,7 @@ const mockStandards = [
|
||||
|
||||
// Функция для получения данных эталона по ID
|
||||
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
|
||||
placeholder={element.placeholder}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
required={element.required}
|
||||
/>
|
||||
)
|
||||
@@ -147,7 +136,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
<Textarea
|
||||
placeholder={element.placeholder}
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
required={element.required}
|
||||
rows={3}
|
||||
/>
|
||||
@@ -159,7 +148,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
type="number"
|
||||
placeholder={element.placeholder}
|
||||
value={value || ''}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
onChange(e.target.value ? parseFloat(e.target.value) : '')
|
||||
}
|
||||
required={element.required}
|
||||
@@ -171,7 +160,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
<Input
|
||||
type="date"
|
||||
value={value || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
required={element.required}
|
||||
/>
|
||||
)
|
||||
@@ -185,7 +174,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{element.options?.map((option) => (
|
||||
{element.options?.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
@@ -197,7 +186,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
case 'radio':
|
||||
return (
|
||||
<RadioGroup value={value || ''} onValueChange={onChange}>
|
||||
{element.options?.map((option) => (
|
||||
{element.options?.map(option => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={option.value}
|
||||
@@ -231,7 +220,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
case 'standards':
|
||||
const selectedStandards = Array.isArray(value) ? value : []
|
||||
const selectedStandardsData = selectedStandards
|
||||
.map((id) => getStandardById(id))
|
||||
.map(id => getStandardById(id))
|
||||
.filter(Boolean)
|
||||
|
||||
return (
|
||||
@@ -282,7 +271,7 @@ const FormElement: FC<FormElementProps> = ({ element, value, onChange }) => {
|
||||
{getStandardTypeBadge(standard.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -379,7 +368,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
const [formData, setFormData] = useState<Record<string, any>>({})
|
||||
|
||||
const handleFieldChange = (elementId: string, value: any) => {
|
||||
setFormData((prev) => ({
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[elementId]: value,
|
||||
}))
|
||||
@@ -388,7 +377,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
const handleSave = () => {
|
||||
// Ищем элемент с названием протокола (должен иметь специальный тип или название)
|
||||
const protocolNameElement = template.elements.find(
|
||||
(el) => el.name === 'protocolName' || el.label === 'Название протокола',
|
||||
el => el.name === 'protocolName' || el.label === 'Название протокола'
|
||||
)
|
||||
const protocolName = protocolNameElement
|
||||
? formData[protocolNameElement.id]
|
||||
@@ -420,8 +409,8 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
const contentHeight = Math.max(
|
||||
0,
|
||||
...template.elements.map(
|
||||
(el) => (el.layout?.y || 0) + (el.layout?.height || 0),
|
||||
),
|
||||
el => (el.layout?.y || 0) + (el.layout?.height || 0)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -432,7 +421,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
|
||||
// Проверяем есть ли кнопка сохранения (есть ли название протокола)
|
||||
const protocolNameElement = template.elements.find(
|
||||
(el) => el.name === 'protocolName' || el.label === 'Название протокола',
|
||||
el => el.name === 'protocolName' || el.label === 'Название протокола'
|
||||
)
|
||||
const canSave = protocolNameElement
|
||||
? !!formData[protocolNameElement.id]?.trim()
|
||||
@@ -473,7 +462,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
className="relative mx-auto my-8"
|
||||
style={{ width: canvasSize.width, height: canvasSize.height }}
|
||||
>
|
||||
{template.elements.map((element) => {
|
||||
{template.elements.map(element => {
|
||||
const layout = element.layout || {
|
||||
x: 50,
|
||||
y: 50,
|
||||
@@ -495,7 +484,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
||||
<FormElement
|
||||
element={element}
|
||||
value={formData[element.id]}
|
||||
onChange={(value) => handleFieldChange(element.id, value)}
|
||||
onChange={value => handleFieldChange(element.id, value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -528,25 +517,19 @@ export const ProtocolCreation: FC = () => {
|
||||
const { templateId } = useParams<{ templateId?: string }>()
|
||||
const { templates } = useTemplateContext()
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
// Автоматически выбираем шаблон если передан ID в URL
|
||||
useEffect(() => {
|
||||
if (templateId && templates.length > 0) {
|
||||
const template = templates.find((t) => t.id === templateId)
|
||||
const template = templates.find(t => t.id === templateId)
|
||||
if (template) {
|
||||
setSelectedTemplate(template)
|
||||
}
|
||||
}
|
||||
}, [templateId, templates])
|
||||
|
||||
const handleTemplateSelect = (template: Template) => {
|
||||
setSelectedTemplate(template)
|
||||
// Обновляем URL с ID выбранного шаблона
|
||||
navigate(`/create-protocol/${template.id}`)
|
||||
}
|
||||
|
||||
const handleProtocolSave = (data: Record<string, any>) => {
|
||||
console.log('Сохранение протокола:', data)
|
||||
// Здесь будет логика сохранения протокола
|
||||
@@ -555,12 +538,28 @@ export const ProtocolCreation: FC = () => {
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedTemplate(null)
|
||||
// Возвращаемся к списку шаблонов
|
||||
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 (
|
||||
<ProtocolForm
|
||||
template={selectedTemplate}
|
||||
@@ -568,91 +567,4 @@ export const ProtocolCreation: FC = () => {
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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 { useParams } from 'react-router-dom'
|
||||
import { TemplateManager } from '../../../../components/TemplateManager/TemplateManager'
|
||||
import { useTemplateContext } from '../../../../contexts/TemplateContext'
|
||||
|
||||
export const TemplateSetup: FC = () => {
|
||||
const { templateId } = useParams<{ templateId: string }>()
|
||||
@@ -9,7 +9,7 @@ export const TemplateSetup: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
const template = templates.find((t) => t.id === templateId)
|
||||
const template = templates.find(t => t.id === templateId)
|
||||
if (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 { NoMatch } from './NoMatch'
|
||||
import { ProtocolCreation } from './ProtocolCreation'
|
||||
import { TemplateSetup } from './TemplateSetup'
|
||||
|
||||
export { ElementsCreation, NoMatch, ProtocolCreation, TemplateSetup }
|
||||
export { ElementsCreation, ProtocolCreation, TemplateSetup }
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
SaveFormulasResponse,
|
||||
SyncStatus,
|
||||
TemplateFormulaData,
|
||||
} from '../types/formula-api'
|
||||
} from '@/types/formula-api'
|
||||
|
||||
/**
|
||||
* Сервис для работы с API формул электронных таблиц
|
||||
@@ -31,7 +31,7 @@ class FormulaApiService {
|
||||
* Сохранение формул на сервер
|
||||
*/
|
||||
async saveFormulas(
|
||||
request: SaveFormulasRequest,
|
||||
request: SaveFormulasRequest
|
||||
): Promise<SaveFormulasResponse> {
|
||||
this.emitEvent({ type: 'SAVE_START', templateId: request.templateId })
|
||||
|
||||
@@ -123,7 +123,7 @@ class FormulaApiService {
|
||||
* Загрузка сохраненных формул
|
||||
*/
|
||||
async loadFormulas(
|
||||
request: LoadFormulasRequest,
|
||||
request: LoadFormulasRequest
|
||||
): Promise<LoadFormulasResponse> {
|
||||
this.emitEvent({ type: 'LOAD_START', templateId: request.templateId })
|
||||
|
||||
@@ -205,7 +205,7 @@ class FormulaApiService {
|
||||
* Получение истории изменений формул
|
||||
*/
|
||||
async getFormulaHistory(
|
||||
request: FormulaHistoryRequest,
|
||||
request: FormulaHistoryRequest
|
||||
): Promise<FormulaHistoryResponse> {
|
||||
try {
|
||||
// Имитация задержки API
|
||||
@@ -306,7 +306,7 @@ class FormulaApiService {
|
||||
*/
|
||||
addEventListener(
|
||||
eventType: string,
|
||||
listener: (event: FormulaApiEvent) => void,
|
||||
listener: (event: FormulaApiEvent) => void
|
||||
): void {
|
||||
if (!this.eventListeners.has(eventType)) {
|
||||
this.eventListeners.set(eventType, [])
|
||||
@@ -319,7 +319,7 @@ class FormulaApiService {
|
||||
*/
|
||||
removeEventListener(
|
||||
eventType: string,
|
||||
listener: (event: FormulaApiEvent) => void,
|
||||
listener: (event: FormulaApiEvent) => void
|
||||
): void {
|
||||
const listeners = this.eventListeners.get(eventType)
|
||||
if (listeners) {
|
||||
@@ -335,7 +335,7 @@ class FormulaApiService {
|
||||
*/
|
||||
private emitEvent(event: FormulaApiEvent): void {
|
||||
const listeners = this.eventListeners.get(event.type) || []
|
||||
listeners.forEach((listener) => {
|
||||
listeners.forEach(listener => {
|
||||
try {
|
||||
listener(event)
|
||||
} catch (error) {
|
||||
@@ -349,7 +349,7 @@ class FormulaApiService {
|
||||
*/
|
||||
private async delay(min: number, max: number): Promise<void> {
|
||||
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": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"noImplicitAny": true,
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"target": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path */
|
||||
"baseUrl": "./",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"strictNullChecks": true,
|
||||
"outDir": "./dist/",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"shared/*": ["src/shared/*"],
|
||||
"entities/*": ["src/entities/*"],
|
||||
"features/*": ["src/features/*"],
|
||||
"widgets/*": ["src/components/*"],
|
||||
"pages/*": ["src/pages/*"],
|
||||
"processes/*": ["src/processes/*"],
|
||||
"app/*": ["src/app/*"]
|
||||
"@/*": ["./src/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src", "types/vite-env.d.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"ts-node": {
|
||||
"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 path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
||||
Reference in New Issue
Block a user