2 окна и настройка элементов

This commit is contained in:
Artyom Tsyrulnikov
2025-07-15 09:23:14 +03:00
parent 6bd5bba15a
commit 4a38c3e3ba
19 changed files with 695 additions and 338 deletions

View File

@@ -17,8 +17,8 @@ type SheetType = 'report' | 'calculations';
const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
const multiSheetEngine = useMultiSheetEngine({
sheets: [
{ name: 'Report', rows: 20, cols: 10 },
{ name: 'Calculations', rows: 20, cols: 10 }
{ name: 'Report', rows: 50, cols: 20 },
{ name: 'Calculations', rows: 50, cols: 20 }
],
initialData: templateData || {}
});
@@ -27,16 +27,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Объединенные состояния для обеих таблиц
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
report: Array(20).fill(null).map(() =>
Array(10).fill(null).map(() => ({
report: Array(50).fill(null).map(() =>
Array(20).fill(null).map(() => ({
value: '',
isSelected: false,
isModified: false,
templateValue: ''
}))
),
calculations: Array(20).fill(null).map(() =>
Array(10).fill(null).map(() => ({
calculations: Array(50).fill(null).map(() =>
Array(20).fill(null).map(() => ({
value: '',
isSelected: false,
isModified: false
@@ -52,11 +52,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Объединенные размеры колонок
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>({
report: Array(10).fill(96),
calculations: Array(10).fill(96)
report: Array(20).fill(96),
calculations: Array(20).fill(96)
});
const [rowHeights] = useState<number[]>(Array(20).fill(32));
const [rowHeights] = useState<number[]>(Array(50).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(20).fill(null).map(() => Array(10).fill(null));
inputRefs.current.calculations = Array(20).fill(null).map(() => Array(10).fill(null));
inputRefs.current.report = Array(50).fill(null).map(() => Array(20).fill(null));
inputRefs.current.calculations = Array(50).fill(null).map(() => Array(20).fill(null));
}, []);
// Загрузка шаблонных данных
@@ -99,16 +99,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
}));
// Загружаем данные в движок
Object.entries(templateData.Report).forEach(([cellName, value]) => {
const match = cellName.match(/^([A-Z]+)(\d+)$/);
if (match) {
const col = match[1].charCodeAt(0) - 65;
const row = parseInt(match[2]) - 1;
if (row >= 0 && row < 20 && col >= 0 && col < 10) {
multiSheetEngine.setCellValue('Report', row, col, String(value));
Object.entries(templateData.Report).forEach(([cellName, value]) => {
const match = cellName.match(/^([A-Z]+)(\d+)$/);
if (match) {
const col = match[1].charCodeAt(0) - 65;
const row = parseInt(match[2]) - 1;
if (row >= 0 && row < 50 && col >= 0 && col < 20) {
multiSheetEngine.setCellValue('Report', row, col, String(value));
}
}
}
});
});
}
}, [templateData]);
@@ -264,8 +264,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
const moveSelection = (deltaRow: number, deltaCol: number) => {
if (!selectedCell) return;
const newRow = Math.max(0, Math.min(19, selectedCell.row + deltaRow));
const newCol = Math.max(0, Math.min(9, selectedCell.col + deltaCol));
const newRow = Math.max(0, Math.min(49, selectedCell.row + deltaRow));
const newCol = Math.max(0, Math.min(19, selectedCell.col + deltaCol));
setSelectedCell({ row: newRow, col: newCol });
setIsEditing(false);
@@ -413,17 +413,26 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
// Универсальный рендер таблицы
const renderSpreadsheet = (sheetType: SheetType) => {
const cells = sheetData[sheetType];
const sheetName = sheetType === 'report' ? 'Report' : 'Calculations';
const data = sheetData[sheetType];
const widths = columnWidths[sheetType];
return (
<div className="overflow-auto h-full">
<table className="border-collapse" style={{ minWidth: `${48 + widths.reduce((a, b) => a + b, 0)}px` }}>
<div
ref={el => containerRefs.current[sheetType] = el}
className="bg-white rounded-lg shadow-md overflow-auto border border-gray-300"
style={{
width: '100%',
height: '100%',
position: 'relative',
}}
onKeyDown={handleKeyDown}
tabIndex={0}
>
<table className="w-full border-collapse" style={{ tableLayout: 'fixed' }}>
<thead>
<tr>
<th className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium"></th>
{Array.from({ length: 10 }, (_, i) => (
{Array.from({ length: 20 }, (_, i) => (
<th
key={i}
className="h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center relative"
@@ -456,101 +465,65 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
</tr>
</thead>
<tbody>
{cells.map((row, rowIndex) => (
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
<td className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center">
{rowIndex + 1}
</td>
{row.map((cell, colIndex) => (
<td
<td
key={colIndex}
className={`border border-gray-300 relative ${
selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
? ''
: 'hover:bg-gray-50'
className={`border border-gray-200 ${
(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) ? 'bg-blue-100' : ''
} ${
cell.isModified ? 'bg-yellow-50' : ''
}`}
style={{
width: widths[colIndex],
height: rowHeights[rowIndex],
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
? { boxShadow: isEditing
? 'inset 0 0 0 2px #3b82f6, inset 0 0 0 4px rgba(59, 130, 246, 0.3)'
: 'inset 0 0 0 2px #3b82f6'
}
: {})
style={{
minWidth: `${widths[colIndex]}px`,
width: `${widths[colIndex]}px`,
height: `${rowHeights[rowIndex]}px`,
overflow: 'hidden',
padding: 0,
position: 'relative'
}}
onClick={() => {
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) {
return;
}
if (activeSheet !== sheetType) {
setActiveSheet(sheetType);
}
if (activeSheet !== sheetType) setActiveSheet(sheetType);
handleCellClick(rowIndex, colIndex);
}}
onDoubleClick={() => {
if (activeSheet !== sheetType) {
setActiveSheet(sheetType);
}
handleCellDoubleClick(rowIndex, colIndex);
}}
onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)}
>
<input
type="text"
className={`w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none ${
isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
? 'cursor-text'
: 'cursor-cell'
} ${
cell.isModified ? 'text-orange-700 font-medium' : ''
}`}
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
? cell.value
: String(multiSheetEngine.getCellValue(sheetName, rowIndex, colIndex) || '')}
onChange={(e) => {
const newValue = e.target.value;
handleCellChange(rowIndex, colIndex, newValue, false);
setFormulaBarValue(newValue);
// Отслеживаем изменения в ячейке
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) {
setHasEditingChanges(newValue !== initialEditingValue);
}
}}
onFocus={(e) => {
if (activeSheet !== sheetType) {
setActiveSheet(sheetType);
}
setSelectedCell({ row: rowIndex, col: colIndex });
if (!isEditing || activeSheet !== sheetType) {
(e.target as HTMLInputElement).blur();
}
}}
onBlur={() => stopEditing(true)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
stopEditing(true);
moveSelection(1, 0);
} else if (e.key === 'Escape') {
e.preventDefault();
stopEditing(false);
}
}}
onMouseUp={(e) => {
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) {
e.stopPropagation();
}
}}
readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex || activeSheet !== sheetType}
ref={(el) => {
if (inputRefs.current[sheetType][rowIndex]) {
inputRefs.current[sheetType][rowIndex][colIndex] = el;
}
}}
/>
{isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex ? (
<input
ref={el => {
if (inputRefs.current[sheetType][rowIndex]) {
inputRefs.current[sheetType][rowIndex][colIndex] = el;
}
}}
type="text"
value={sheetData[sheetType][rowIndex][colIndex].value}
onChange={(e) => handleCellChange(rowIndex, colIndex, e.target.value)}
onBlur={() => stopEditing(true)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
stopEditing(true);
} else if (e.key === 'Escape') {
stopEditing(false);
}
}}
className="absolute top-0 left-0 w-full h-full p-2 border-2 border-blue-500 z-20 box-border"
/>
) : (
<div
className="relative w-full h-full p-2 truncate"
style={{
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
? { boxShadow: 'inset 0 0 0 2px #3b82f6' }
: {})
}}
>
{String(multiSheetEngine.getCellValue(sheetType === 'report' ? 'Report' : 'Calculations', rowIndex, colIndex) || '')}
</div>
)}
</td>
))}
</tr>
@@ -562,73 +535,36 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
};
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>
<h1 className="text-xl font-semibold text-gray-800">Система отчетов</h1>
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-600">
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-yellow-50 border border-yellow-200 rounded"></div>
<span>Измененные ячейки</span>
</div>
<div className="flex items-center space-x-1">
<div className="w-3 h-3 bg-white border border-gray-200 rounded"></div>
<span>Шаблонные данные</span>
</div>
<div className="flex items-center space-x-1">
<span className="text-xs text-gray-500">Ctrl+Z - восстановить шаблон</span>
</div>
</div>
</div>
<button
onClick={handleExportChanges}
className="px-4 py-2 rounded-md text-sm font-medium bg-green-500 text-white hover:bg-green-600 transition-colors"
>
Экспорт изменений
</button>
</div>
</div>
<div className="flex flex-col h-full space-y-4 p-4 bg-gray-100">
{/* Двойная панель */}
<div className="flex-1 flex min-h-0">
<div className="flex-grow flex space-x-4 overflow-hidden">
{/* Левая панель - Отчет */}
<div className="w-1/2 border-r border-gray-200 flex flex-col">
<div className="bg-gray-100 px-4 py-2 border-b border-gray-200 flex-shrink-0">
<h2 className="text-sm font-medium text-gray-700">Отчет</h2>
</div>
<FormulaBar sheetType="report" />
<div
ref={(el) => containerRefs.current.report = el}
className="flex-1 bg-white outline-none overflow-hidden"
tabIndex={activeSheet === 'report' ? 0 : -1}
onKeyDown={activeSheet === 'report' ? handleKeyDown : undefined}
onClick={() => setActiveSheet('report')}
>
{renderSpreadsheet('report')}
</div>
<div className="w-1/2 flex flex-col">
<h2 className="text-lg font-semibold mb-2">Отчет</h2>
{renderSpreadsheet('report')}
</div>
{/* Правая панель - Вычисления */}
<div className="w-1/2 flex flex-col">
<div className="bg-gray-100 px-4 py-2 border-b border-gray-200 flex-shrink-0">
<h2 className="text-sm font-medium text-gray-700">Вычисления</h2>
</div>
<FormulaBar sheetType="calculations" />
<div
ref={(el) => containerRefs.current.calculations = el}
className="flex-1 bg-white outline-none overflow-hidden"
tabIndex={activeSheet === 'calculations' ? 0 : -1}
onKeyDown={activeSheet === 'calculations' ? handleKeyDown : undefined}
onClick={() => setActiveSheet('calculations')}
>
{renderSpreadsheet('calculations')}
</div>
<h2 className="text-lg font-semibold mb-2">Расчеты</h2>
{renderSpreadsheet('calculations')}
</div>
</div>
<div className="flex-shrink-0">
<FormulaBar sheetType={activeSheet} />
</div>
<div className="flex-shrink-0 flex justify-end items-center">
<button
onClick={handleExportChanges}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Экспорт изменений
</button>
</div>
</div>
);
};
export default DualSpreadsheet;
export default DualSpreadsheet;

View File

@@ -4,7 +4,7 @@ import { ElementType, TemplateElement } from '../../types/template';
import { Button } from '../ui/button';
import { Card, CardContent, CardHeader } from '../ui/card';
import { Checkbox } from '../ui/checkbox';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
@@ -231,9 +231,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
};
return (
<div className="p-4 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Конструктор элементов</h3>
<div className="h-full flex flex-col">
<div className="flex items-center justify-between p-4 bg-white">
<h3 className="text-lg font-semibold">Элементы формы</h3>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" onClick={() => resetForm()}>
@@ -244,6 +244,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Добавить элемент</DialogTitle>
<DialogDescription>
Создайте новый элемент формы для связи с ячейкой Excel
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 mt-4">
@@ -256,63 +259,82 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</Dialog>
</div>
<div className="flex-1 overflow-y-auto pr-2 -mr-2">
{elements.map(element => (
<Card key={element.id} className="mb-4">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex items-center gap-2">
{ELEMENT_TYPES.find(t => t.type === element.type)?.icon}
<p className="font-semibold">{element.label}</p>
</div>
<div className="flex items-center gap-2">
<Dialog
open={editingElement?.id === element.id}
onOpenChange={isOpen => !isOpen && handleCancelEdit()}
>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => handleEditElement(element)}>
<Edit className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={handleCancelEdit}>
Отмена
<div className="flex-1 overflow-y-auto p-4 bg-white">
{elements.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="rounded-full bg-gray-100 p-4 mb-4">
<Plus className="h-8 w-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Нет элементов интерфейса</h3>
<p className="text-sm text-gray-500 mb-4">
Создайте элементы формы для связи с ячейками Excel
</p>
<Button onClick={() => setIsAddDialogOpen(true)} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Добавить первый элемент
</Button>
</div>
) : (
elements.map(element => (
<Card key={element.id} className="mb-4">
<CardHeader className="flex flex-row items-center justify-between p-4">
<div className="flex items-center gap-2">
{ELEMENT_TYPES.find(t => t.type === element.type)?.icon}
<p className="font-semibold">{element.label}</p>
</div>
<div className="flex items-center gap-2">
<Dialog
open={editingElement?.id === element.id}
onOpenChange={isOpen => !isOpen && handleCancelEdit()}
>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => handleEditElement(element)}>
<Edit className="h-4 w-4" />
</Button>
<Button onClick={handleUpdateElement}>Сохранить</Button>
</div>
</DialogContent>
</Dialog>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle>
<DialogDescription>
Измените настройки элемента формы
</DialogDescription>
</DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={handleCancelEdit}>
Отмена
</Button>
<Button onClick={handleUpdateElement}>Сохранить</Button>
</div>
</DialogContent>
</Dialog>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-600"
onClick={() => onElementDelete(element.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="text-sm text-muted-foreground">
<p>
<strong>Тип:</strong> {ELEMENT_TYPES.find(t => t.type === element.type)?.label}
</p>
<p>
<strong>Имя:</strong> {element.name}
</p>
<p>
<strong>Ячейка:</strong> {element.targetCell}
</p>
</div>
</CardContent>
</Card>
))}
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-600"
onClick={() => onElementDelete(element.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="p-4 pt-0">
<div className="text-sm text-muted-foreground">
<p>
<strong>Тип:</strong> {ELEMENT_TYPES.find(t => t.type === element.type)?.label}
</p>
<p>
<strong>Имя:</strong> {element.name}
</p>
<p>
<strong>Ячейка:</strong> {element.targetCell}
</p>
</div>
</CardContent>
</Card>
))
)}
</div>
</div>
);

View File

@@ -1,10 +1,10 @@
import { ArrowLeft, Save, Upload } from 'lucide-react';
import { ArrowLeft, Save, Upload, Wrench } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import * as XLSX from 'xlsx';
import { Template, TemplateElement } from '../../types/template';
import { Template } from '../../types/template';
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet';
import { Button } from '../ui/button';
import { ElementConstructor } from './ElementConstructor';
interface TemplateEditorProps {
template: Template;
@@ -22,6 +22,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
const [editedTemplate, setEditedTemplate] = useState<Template>(template);
const [excelData, setExcelData] = useState<Record<string, any>>(template.excelData || {});
const fileInputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
@@ -70,36 +71,14 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
reader.readAsArrayBuffer(file);
};
const handleElementAdd = (element: TemplateElement) => {
setEditedTemplate(prev => ({
...prev,
elements: [...prev.elements, element],
updatedAt: new Date(),
}));
};
const handleElementUpdate = (elementId: string, updates: Partial<TemplateElement>) => {
setEditedTemplate(prev => ({
...prev,
elements: prev.elements.map(el =>
el.id === elementId ? { ...el, ...updates } : el
),
updatedAt: new Date(),
}));
};
const handleElementDelete = (elementId: string) => {
setEditedTemplate(prev => ({
...prev,
elements: prev.elements.filter(el => el.id !== elementId),
updatedAt: new Date(),
}));
};
const handleSave = () => {
onSave(editedTemplate);
};
const handleCreateElements = () => {
navigate('/elements');
};
return (
<div className="h-screen flex flex-col bg-gray-50">
{/* Заголовок */}
@@ -119,6 +98,14 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCreateElements}
className="flex items-center gap-2"
>
<Wrench className="h-4 w-4" />
Создать элементы
</Button>
<Button variant="outline" onClick={onCancel}>
Отмена
</Button>
@@ -130,64 +117,51 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
</div>
</div>
{/* Основной контент */}
<div className="flex-1 flex flex-col min-h-0">
{/* Загрузка Excel и таблицы */}
<div className="flex-1 flex flex-col">
{/* Панель загрузки Excel */}
<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">
<div className="text-sm font-medium text-gray-700">
Excel шаблон:
</div>
{editedTemplate.excelFile ? (
<div className="flex items-center gap-2 text-sm text-green-600">
<Upload className="h-4 w-4" />
<span>{editedTemplate.excelFile.name}</span>
</div>
) : (
<div className="text-sm text-gray-500">Файл не загружен</div>
)}
</div>
<div>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileUpload}
className="hidden"
/>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2"
>
<Upload className="h-4 w-4" />
{editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'}
</Button>
</div>
{/* Панель загрузки Excel */}
<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">
<div className="text-sm font-medium text-gray-700">
Excel шаблон:
</div>
{editedTemplate.excelFile ? (
<div className="flex items-center gap-2 text-sm text-green-600">
<Upload className="h-4 w-4" />
<span>{editedTemplate.excelFile.name}</span>
</div>
) : (
<div className="text-sm text-gray-500">Файл не загружен</div>
)}
</div>
{/* Таблицы */}
<div className="flex-1 min-h-0">
<DualSpreadsheet
templateData={{ Report: excelData }}
<div className="flex items-center gap-2">
<div className="text-sm text-gray-500">
Элементов создано: {editedTemplate.elements.length}
</div>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileUpload}
className="hidden"
/>
<Button
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2"
>
<Upload className="h-4 w-4" />
{editedTemplate.excelFile ? 'Заменить файл' : 'Загрузить Excel'}
</Button>
</div>
</div>
</div>
{/* Конструктор элементов */}
<div className="h-80 border-t border-gray-200 bg-white flex-shrink-0">
<ElementConstructor
elements={editedTemplate.elements}
onElementAdd={handleElementAdd}
onElementUpdate={handleElementUpdate}
onElementDelete={handleElementDelete}
/>
</div>
{/* Таблицы - теперь занимают всё оставшееся пространство */}
<div className="flex-1 min-h-0">
<DualSpreadsheet
templateData={{ Report: excelData }}
/>
</div>
</div>
);

View File

@@ -1,9 +1,10 @@
import { FileText, Plus, Settings, Upload } from 'lucide-react';
import React, { useState } from 'react';
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, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
import { Input } from '../ui/input';
import { TemplateEditor } from './TemplateEditor';
import { TemplateRenderer } from './TemplateRenderer';
@@ -13,7 +14,7 @@ interface TemplateManagerProps {
}
export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSelect }) => {
const [templates, setTemplates] = useState<Template[]>([]);
const { templates, addTemplate, updateTemplate, deleteTemplate } = useTemplateContext();
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
@@ -32,7 +33,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
updatedAt: new Date(),
};
setTemplates(prev => [...prev, newTemplate]);
addTemplate(newTemplate);
setSelectedTemplate(newTemplate);
setIsEditMode(true);
setIsCreateDialogOpen(false);
@@ -41,14 +42,12 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
};
const handleTemplateUpdate = (updatedTemplate: Template) => {
setTemplates(prev => prev.map(t =>
t.id === updatedTemplate.id ? updatedTemplate : t
));
updateTemplate(updatedTemplate);
setSelectedTemplate(updatedTemplate);
};
const handleTemplateDelete = (templateId: string) => {
setTemplates(prev => prev.filter(t => t.id !== templateId));
deleteTemplate(templateId);
if (selectedTemplate?.id === templateId) {
setSelectedTemplate(null);
setIsEditMode(false);
@@ -94,8 +93,8 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
<div className="p-6 max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Система протоколов</h1>
<p className="text-gray-600 mt-2">Управление шаблонами и создание отчетов</p>
<h1 className="text-3xl font-bold text-gray-900">Настройка шаблонов</h1>
<p className="text-gray-600 mt-2">Создание и управление шаблонами протоколов</p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
@@ -108,6 +107,9 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Создать новый шаблон</DialogTitle>
<DialogDescription>
Введите название и описание для нового шаблона.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
@@ -206,7 +208,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
className="flex-1"
onClick={() => handleTemplateSelect(template)}
>
Использовать
Предпросмотр
</Button>
<Button
variant="outline"

View File

@@ -14,12 +14,14 @@ 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>>({});
@@ -278,7 +280,7 @@ export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
<div className="flex-1 flex min-h-0">
{/* Левая панель - форма */}
<div className="w-80 border-r border-gray-200 bg-white flex flex-col">
<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>
@@ -295,21 +297,34 @@ export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
{errors[element.id] && (
<p className="text-sm text-red-600">{errors[element.id]}</p>
)}
<div className="text-xs text-gray-500">
Ячейка: {element.targetCell}
</div>
{!hideSpreadsheet && (
<div className="text-xs text-gray-500">
Ячейка: {element.targetCell}
</div>
)}
</div>
))}
</div>
</div>
</div>
{/* Правая панель - таблица */}
<div className="flex-1 min-h-0">
<DualSpreadsheet
templateData={{ Report: spreadsheetData }}
/>
</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>
);