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

This commit is contained in:
Artyom Tsyrulnikov
2025-07-15 14:28:56 +03:00
parent 4a38c3e3ba
commit df12bde43f
5 changed files with 16 additions and 394 deletions

View File

@@ -8,15 +8,15 @@ const Layout: FC = () => {
return ( return (
<div className="h-screen flex flex-col bg-gray-50"> <div className="h-screen flex flex-col bg-gray-50">
{/* Верхняя навигация */} {/* Верхняя навигация */}
<nav className="bg-white border-b border-gray-200 px-6 py-4"> <nav className="bg-white border-b border-gray-200 px-6 py-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
<h1 className="text-xl font-bold text-gray-900">Система протоколов</h1> <h1 className="text-xl font-bold text-gray-900">Protoc</h1>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Link <Link
to="/templates" to="/templates"
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center gap-2 px-3 py-1 rounded-md text-sm font-medium transition-colors ${
location.pathname === '/templates' location.pathname === '/templates'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
@@ -28,7 +28,7 @@ const Layout: FC = () => {
<Link <Link
to="/elements" to="/elements"
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center gap-2 px-3 py-1 rounded-md text-sm font-medium transition-colors ${
location.pathname === '/elements' location.pathname === '/elements'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
@@ -40,7 +40,7 @@ const Layout: FC = () => {
<Link <Link
to="/create-protocol" to="/create-protocol"
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${ className={`flex items-center gap-2 px-3 py-1 rounded-md text-sm font-medium transition-colors ${
location.pathname === '/create-protocol' location.pathname === '/create-protocol'
? 'bg-blue-100 text-blue-700' ? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'

View File

@@ -17,8 +17,8 @@ type SheetType = 'report' | 'calculations';
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => { const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
const multiSheetEngine = useMultiSheetEngine({ const multiSheetEngine = useMultiSheetEngine({
sheets: [ sheets: [
{ name: 'Report', rows: 50, cols: 20 }, { name: 'Report', rows: 90, cols: 20 },
{ name: 'Calculations', rows: 50, cols: 20 } { name: 'Calculations', rows: 90, cols: 20 }
], ],
initialData: templateData || {} initialData: templateData || {}
}); });
@@ -27,7 +27,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Объединенные состояния для обеих таблиц // Объединенные состояния для обеих таблиц
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({ const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
report: Array(50).fill(null).map(() => report: Array(90).fill(null).map(() =>
Array(20).fill(null).map(() => ({ Array(20).fill(null).map(() => ({
value: '', value: '',
isSelected: false, isSelected: false,
@@ -35,7 +35,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
templateValue: '' templateValue: ''
})) }))
), ),
calculations: Array(50).fill(null).map(() => calculations: Array(90).fill(null).map(() =>
Array(20).fill(null).map(() => ({ Array(20).fill(null).map(() => ({
value: '', value: '',
isSelected: false, isSelected: false,
@@ -56,7 +56,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
calculations: Array(20).fill(96) calculations: Array(20).fill(96)
}); });
const [rowHeights] = useState<number[]>(Array(50).fill(32)); const [rowHeights] = useState<number[]>(Array(90).fill(32));
// Объединенные refs // Объединенные refs
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({ const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
@@ -76,8 +76,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Инициализация refs // Инициализация refs
useEffect(() => { useEffect(() => {
inputRefs.current.report = Array(50).fill(null).map(() => Array(20).fill(null)); inputRefs.current.report = Array(90).fill(null).map(() => Array(20).fill(null));
inputRefs.current.calculations = Array(50).fill(null).map(() => Array(20).fill(null)); inputRefs.current.calculations = Array(90).fill(null).map(() => Array(20).fill(null));
}, []); }, []);
// Загрузка шаблонных данных // Загрузка шаблонных данных

View File

@@ -7,7 +7,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { TemplateEditor } from './TemplateEditor'; import { TemplateEditor } from './TemplateEditor';
import { TemplateRenderer } from './TemplateRenderer';
interface TemplateManagerProps { interface TemplateManagerProps {
onTemplateSelect?: (template: Template) => void; onTemplateSelect?: (template: Template) => void;
@@ -65,16 +64,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
setIsEditMode(true); setIsEditMode(true);
}; };
if (selectedTemplate && !isEditMode) {
return (
<TemplateRenderer
template={selectedTemplate}
onBack={() => setSelectedTemplate(null)}
onEdit={() => setIsEditMode(true)}
/>
);
}
if (selectedTemplate && isEditMode) { if (selectedTemplate && isEditMode) {
return ( return (
<TemplateEditor <TemplateEditor
@@ -104,6 +93,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
Создать шаблон Создать шаблон
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Создать новый шаблон</DialogTitle> <DialogTitle>Создать новый шаблон</DialogTitle>
@@ -198,7 +188,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
<span>{template.elements.length} элементов интерфейса</span> <span>{template.elements.length} элементов интерфейса</span>
</div> </div>
<div className="text-xs text-gray-500 mt-2"> <div className="text-xs text-gray-500 mt-2">
Создан: {template.createdAt.toLocaleDateString()} Создан: {template.createdAt.toLocaleDateString('ru-RU')}
</div> </div>
</div> </div>
<div className="flex gap-2 mt-4"> <div className="flex gap-2 mt-4">
@@ -210,16 +200,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
> >
Предпросмотр Предпросмотр
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleEditTemplate(template);
}}
>
Настроить
</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,331 +0,0 @@
import { ArrowLeft, Send, Settings } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { ExportData, Template, TemplateElement } from '../../types/template';
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Checkbox } from '../ui/checkbox';
import { Input } from '../ui/input';
import { RadioGroup, RadioGroupItem } from '../ui/radio-group';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Textarea } from '../ui/textarea';
interface TemplateRendererProps {
template: Template;
onBack: () => void;
onEdit: () => void;
hideSpreadsheet?: boolean;
}
export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
template,
onBack,
onEdit,
hideSpreadsheet = false,
}) => {
const [formData, setFormData] = useState<Record<string, any>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [spreadsheetData, setSpreadsheetData] = useState<Record<string, any>>(template.excelData || {});
// Инициализация формы значениями по умолчанию
useEffect(() => {
const initialData: Record<string, any> = {};
template.elements.forEach(element => {
if (element.defaultValue) {
initialData[element.id] = element.defaultValue;
}
});
setFormData(initialData);
}, [template.elements]);
// Обновление данных таблицы при изменении формы
useEffect(() => {
const updatedData = { ...template.excelData || {}, ...spreadsheetData };
template.elements.forEach(element => {
const value = formData[element.id];
if (value !== undefined && value !== null && value !== '') {
updatedData[element.targetCell] = value;
}
});
setSpreadsheetData(updatedData);
}, [formData, template.elements, template.excelData]);
const handleInputChange = (elementId: string, value: any) => {
setFormData(prev => ({
...prev,
[elementId]: value,
}));
// Очищаем ошибку при изменении значения
if (errors[elementId]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[elementId];
return newErrors;
});
}
};
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
template.elements.forEach(element => {
if (element.required) {
const value = formData[element.id];
if (!value || (typeof value === 'string' && value.trim() === '')) {
newErrors[element.id] = `Поле "${element.label}" обязательно для заполнения`;
}
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (!validateForm()) {
return;
}
// Получаем измененные ячейки из таблицы
const changedCells = (window as any).getChangedCells?.() || {};
// Создаем данные для экспорта
const exportData: ExportData = {
templateId: template.id,
templateName: template.name,
excelFile: template.excelFile!,
cellUpdates: {
...changedCells,
// Добавляем значения из формы
...template.elements.reduce((acc, element) => {
const value = formData[element.id];
if (value !== undefined && value !== null && value !== '') {
acc[element.targetCell] = { rawValue: value, calculatedValue: value };
}
return acc;
}, {} as Record<string, any>),
},
};
console.log('Данные для отправки на бэкенд:', exportData);
// Здесь будет отправка на бэкенд
alert(`Отчет "${template.name}" успешно сформирован!\n\роверьте консоль для подробностей.`);
};
const renderFormElement = (element: TemplateElement) => {
const value = formData[element.id] || '';
const hasError = !!errors[element.id];
const commonProps = {
className: hasError ? 'border-red-500' : '',
};
switch (element.type) {
case 'text':
return (
<Input
{...commonProps}
type="text"
placeholder={element.placeholder}
value={value}
onChange={(e) => handleInputChange(element.id, e.target.value)}
/>
);
case 'textarea':
return (
<Textarea
{...commonProps}
placeholder={element.placeholder}
value={value}
onChange={(e) => handleInputChange(element.id, e.target.value)}
/>
);
case 'number':
return (
<Input
{...commonProps}
type="number"
placeholder={element.placeholder}
value={value}
onChange={(e) => handleInputChange(element.id, e.target.value)}
/>
);
case 'date':
return (
<Input
{...commonProps}
type="date"
value={value}
onChange={(e) => handleInputChange(element.id, e.target.value)}
/>
);
case 'select':
return (
<Select value={value} onValueChange={(val) => handleInputChange(element.id, val)}>
<SelectTrigger className={hasError ? 'border-red-500' : ''}>
<SelectValue placeholder={element.placeholder || 'Выберите значение'} />
</SelectTrigger>
<SelectContent>
{element.options?.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
case 'radio':
return (
<RadioGroup value={value} onValueChange={(val) => handleInputChange(element.id, val)}>
{element.options?.map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem value={option.value} id={`${element.id}-${option.value}`} />
<label htmlFor={`${element.id}-${option.value}`} className="text-sm font-medium">
{option.label}
</label>
</div>
))}
</RadioGroup>
);
case 'checkbox':
return (
<div className="flex items-center space-x-2">
<Checkbox
id={element.id}
checked={!!value}
onCheckedChange={(checked) => handleInputChange(element.id, checked)}
/>
<label htmlFor={element.id} className="text-sm font-medium">
{element.placeholder || 'Отметить'}
</label>
</div>
);
default:
return <div>Неизвестный тип элемента: {element.type}</div>;
}
};
if (!template.excelFile) {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold text-gray-900">{template.name}</h1>
<p className="text-gray-600">Шаблон не настроен</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Необходима настройка
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 mb-4">
Для использования этого шаблона необходимо загрузить Excel файл и настроить элементы интерфейса.
</p>
<Button onClick={onEdit}>
Настроить шаблон
</Button>
</CardContent>
</Card>
</div>
);
}
return (
<div className="h-screen flex flex-col bg-gray-50">
{/* Заголовок */}
<div className="bg-white border-b border-gray-200 p-4 flex-shrink-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={onBack}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-semibold text-gray-800">{template.name}</h1>
<p className="text-sm text-gray-600 mt-1">
Заполните форму для создания отчета
</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={onEdit}>
<Settings className="h-4 w-4 mr-2" />
Настроить
</Button>
<Button onClick={handleSubmit} className="flex items-center gap-2">
<Send className="h-4 w-4" />
Сформировать отчет
</Button>
</div>
</div>
</div>
<div className="flex-1 flex min-h-0">
{/* Левая панель - форма */}
<div className={`${hideSpreadsheet ? "w-full" : "w-80 border-r border-gray-200"} bg-white flex flex-col`}>
<div className="p-4 border-b border-gray-200 flex-shrink-0">
<h2 className="text-lg font-semibold text-gray-800">Параметры отчета</h2>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-6">
{template.elements.map((element) => (
<div key={element.id} className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-1">
{element.label}
{element.required && <span className="text-red-500">*</span>}
</label>
{renderFormElement(element)}
{errors[element.id] && (
<p className="text-sm text-red-600">{errors[element.id]}</p>
)}
{!hideSpreadsheet && (
<div className="text-xs text-gray-500">
Ячейка: {element.targetCell}
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* Правая панель - таблица (скрыта если hideSpreadsheet=true) */}
{!hideSpreadsheet && (
<div className="flex-1 min-h-0">
<DualSpreadsheet
templateData={{ Report: spreadsheetData }}
/>
</div>
)}
{/* Скрытый DualSpreadsheet для работы в фоне */}
{hideSpreadsheet && (
<div className="hidden">
<DualSpreadsheet
templateData={{ Report: spreadsheetData }}
/>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,40 +1,13 @@
import { FileText, Plus } from 'lucide-react'; import { FileText, Plus } from 'lucide-react';
import { FC, useState } from 'react'; import { FC } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { TemplateRenderer } from '../../../../components/TemplateManager/TemplateRenderer';
import { Button } from '../../../../components/ui/button'; import { Button } from '../../../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../../components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../../components/ui/card';
import { useTemplateContext } from '../../../../contexts/TemplateContext'; import { useTemplateContext } from '../../../../contexts/TemplateContext';
import { Template } from '../../../../types/template';
export const ProtocolCreation: FC = () => { export const ProtocolCreation: FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { templates } = useTemplateContext(); const { templates } = useTemplateContext();
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template);
};
const handleBack = () => {
setSelectedTemplate(null);
};
const handleEdit = () => {
// Переходим на страницу редактирования шаблона
navigate('/templates');
};
if (selectedTemplate) {
return (
<TemplateRenderer
template={selectedTemplate}
onBack={handleBack}
onEdit={handleEdit}
hideSpreadsheet={true}
/>
);
}
return ( return (
<div className="p-6 max-w-7xl mx-auto"> <div className="p-6 max-w-7xl mx-auto">