ручной рефакторинг
This commit is contained in:
@@ -17,8 +17,8 @@ type SheetType = 'report' | 'calculations';
|
||||
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
const multiSheetEngine = useMultiSheetEngine({
|
||||
sheets: [
|
||||
{ name: 'Report', rows: 50, cols: 20 },
|
||||
{ name: 'Calculations', rows: 50, cols: 20 }
|
||||
{ name: 'Report', rows: 90, cols: 20 },
|
||||
{ name: 'Calculations', rows: 90, cols: 20 }
|
||||
],
|
||||
initialData: templateData || {}
|
||||
});
|
||||
@@ -27,7 +27,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Объединенные состояния для обеих таблиц
|
||||
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
|
||||
report: Array(50).fill(null).map(() =>
|
||||
report: Array(90).fill(null).map(() =>
|
||||
Array(20).fill(null).map(() => ({
|
||||
value: '',
|
||||
isSelected: false,
|
||||
@@ -35,7 +35,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
templateValue: ''
|
||||
}))
|
||||
),
|
||||
calculations: Array(50).fill(null).map(() =>
|
||||
calculations: Array(90).fill(null).map(() =>
|
||||
Array(20).fill(null).map(() => ({
|
||||
value: '',
|
||||
isSelected: false,
|
||||
@@ -56,7 +56,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
calculations: Array(20).fill(96)
|
||||
});
|
||||
|
||||
const [rowHeights] = useState<number[]>(Array(50).fill(32));
|
||||
const [rowHeights] = useState<number[]>(Array(90).fill(32));
|
||||
|
||||
// Объединенные refs
|
||||
const containerRefs = useRef<Record<SheetType, HTMLDivElement | null>>({
|
||||
@@ -76,8 +76,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
||||
|
||||
// Инициализация refs
|
||||
useEffect(() => {
|
||||
inputRefs.current.report = Array(50).fill(null).map(() => Array(20).fill(null));
|
||||
inputRefs.current.calculations = 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(90).fill(null).map(() => Array(20).fill(null));
|
||||
}, []);
|
||||
|
||||
// Загрузка шаблонных данных
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
|
||||
import { Input } from '../ui/input';
|
||||
import { TemplateEditor } from './TemplateEditor';
|
||||
import { TemplateRenderer } from './TemplateRenderer';
|
||||
|
||||
interface TemplateManagerProps {
|
||||
onTemplateSelect?: (template: Template) => void;
|
||||
@@ -65,16 +64,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
||||
setIsEditMode(true);
|
||||
};
|
||||
|
||||
if (selectedTemplate && !isEditMode) {
|
||||
return (
|
||||
<TemplateRenderer
|
||||
template={selectedTemplate}
|
||||
onBack={() => setSelectedTemplate(null)}
|
||||
onEdit={() => setIsEditMode(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTemplate && isEditMode) {
|
||||
return (
|
||||
<TemplateEditor
|
||||
@@ -104,6 +93,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
||||
Создать шаблон
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Создать новый шаблон</DialogTitle>
|
||||
@@ -198,7 +188,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
||||
<span>{template.elements.length} элементов интерфейса</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-2">
|
||||
Создан: {template.createdAt.toLocaleDateString()}
|
||||
Создан: {template.createdAt.toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4">
|
||||
@@ -210,16 +200,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
||||
>
|
||||
Предпросмотр
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditTemplate(template);
|
||||
}}
|
||||
>
|
||||
Настроить
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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\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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user