2 окна и настройка элементов
This commit is contained in:
@@ -1,11 +1,23 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { TemplateManager } from "../components/TemplateManager/TemplateManager";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import { TemplateProvider } from "../contexts/TemplateContext";
|
||||||
|
import { ElementsCreation } from "../pages/ElementsCreation";
|
||||||
|
import { ProtocolCreation } from "../pages/ProtocolCreation";
|
||||||
|
import { TemplateSetup } from "../pages/TemplateSetup";
|
||||||
|
import { Layout } from "./Layout";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<TemplateProvider>
|
||||||
<TemplateManager />
|
<Routes>
|
||||||
</div>
|
<Route path="/" element={<Layout />}>
|
||||||
|
<Route index element={<Navigate to="/templates" replace />} />
|
||||||
|
<Route path="templates" element={<TemplateSetup />} />
|
||||||
|
<Route path="elements" element={<ElementsCreation />} />
|
||||||
|
<Route path="create-protocol" element={<ProtocolCreation />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</TemplateProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,61 @@
|
|||||||
|
import { FileText, Settings, Wrench } from "lucide-react";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen overflow-hidden">
|
<div className="h-screen flex flex-col bg-gray-50">
|
||||||
<main className="h-full">
|
{/* Верхняя навигация */}
|
||||||
|
<nav className="bg-white border-b border-gray-200 px-6 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<h1 className="text-xl font-bold text-gray-900">Система протоколов</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link
|
||||||
|
to="/templates"
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
location.pathname === '/templates'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Настройка шаблонов
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/elements"
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
location.pathname === '/elements'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Wrench className="h-4 w-4" />
|
||||||
|
Создание элементов
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/create-protocol"
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
location.pathname === '/create-protocol'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
Создание протокола
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Основной контент */}
|
||||||
|
<main className="flex-1 overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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: 20, cols: 10 },
|
{ name: 'Report', rows: 50, cols: 20 },
|
||||||
{ name: 'Calculations', rows: 20, cols: 10 }
|
{ name: 'Calculations', rows: 50, cols: 20 }
|
||||||
],
|
],
|
||||||
initialData: templateData || {}
|
initialData: templateData || {}
|
||||||
});
|
});
|
||||||
@@ -27,16 +27,16 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
|
|
||||||
// Объединенные состояния для обеих таблиц
|
// Объединенные состояния для обеих таблиц
|
||||||
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
|
const [sheetData, setSheetData] = useState<Record<SheetType, CellData[][]>>({
|
||||||
report: Array(20).fill(null).map(() =>
|
report: Array(50).fill(null).map(() =>
|
||||||
Array(10).fill(null).map(() => ({
|
Array(20).fill(null).map(() => ({
|
||||||
value: '',
|
value: '',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isModified: false,
|
isModified: false,
|
||||||
templateValue: ''
|
templateValue: ''
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
calculations: Array(20).fill(null).map(() =>
|
calculations: Array(50).fill(null).map(() =>
|
||||||
Array(10).fill(null).map(() => ({
|
Array(20).fill(null).map(() => ({
|
||||||
value: '',
|
value: '',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isModified: false
|
isModified: false
|
||||||
@@ -52,11 +52,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
|
|
||||||
// Объединенные размеры колонок
|
// Объединенные размеры колонок
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>({
|
const [columnWidths, setColumnWidths] = useState<Record<SheetType, number[]>>({
|
||||||
report: Array(10).fill(96),
|
report: Array(20).fill(96),
|
||||||
calculations: Array(10).fill(96)
|
calculations: Array(20).fill(96)
|
||||||
});
|
});
|
||||||
|
|
||||||
const [rowHeights] = useState<number[]>(Array(20).fill(32));
|
const [rowHeights] = useState<number[]>(Array(50).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(20).fill(null).map(() => Array(10).fill(null));
|
inputRefs.current.report = Array(50).fill(null).map(() => Array(20).fill(null));
|
||||||
inputRefs.current.calculations = Array(20).fill(null).map(() => Array(10).fill(null));
|
inputRefs.current.calculations = Array(50).fill(null).map(() => Array(20).fill(null));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Загрузка шаблонных данных
|
// Загрузка шаблонных данных
|
||||||
@@ -104,7 +104,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
if (match) {
|
if (match) {
|
||||||
const col = match[1].charCodeAt(0) - 65;
|
const col = match[1].charCodeAt(0) - 65;
|
||||||
const row = parseInt(match[2]) - 1;
|
const row = parseInt(match[2]) - 1;
|
||||||
if (row >= 0 && row < 20 && col >= 0 && col < 10) {
|
if (row >= 0 && row < 50 && col >= 0 && col < 20) {
|
||||||
multiSheetEngine.setCellValue('Report', row, col, String(value));
|
multiSheetEngine.setCellValue('Report', row, col, String(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,8 +264,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
const moveSelection = (deltaRow: number, deltaCol: number) => {
|
const moveSelection = (deltaRow: number, deltaCol: number) => {
|
||||||
if (!selectedCell) return;
|
if (!selectedCell) return;
|
||||||
|
|
||||||
const newRow = Math.max(0, Math.min(19, selectedCell.row + deltaRow));
|
const newRow = Math.max(0, Math.min(49, selectedCell.row + deltaRow));
|
||||||
const newCol = Math.max(0, Math.min(9, selectedCell.col + deltaCol));
|
const newCol = Math.max(0, Math.min(19, selectedCell.col + deltaCol));
|
||||||
|
|
||||||
setSelectedCell({ row: newRow, col: newCol });
|
setSelectedCell({ row: newRow, col: newCol });
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
@@ -413,17 +413,26 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
|
|
||||||
// Универсальный рендер таблицы
|
// Универсальный рендер таблицы
|
||||||
const renderSpreadsheet = (sheetType: SheetType) => {
|
const renderSpreadsheet = (sheetType: SheetType) => {
|
||||||
const cells = sheetData[sheetType];
|
const data = sheetData[sheetType];
|
||||||
const sheetName = sheetType === 'report' ? 'Report' : 'Calculations';
|
|
||||||
const widths = columnWidths[sheetType];
|
const widths = columnWidths[sheetType];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-full">
|
<div
|
||||||
<table className="border-collapse" style={{ minWidth: `${48 + widths.reduce((a, b) => a + b, 0)}px` }}>
|
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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium"></th>
|
<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
|
<th
|
||||||
key={i}
|
key={i}
|
||||||
className="h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center relative"
|
className="h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center relative"
|
||||||
@@ -456,7 +465,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{cells.map((row, rowIndex) => (
|
{data.map((row, rowIndex) => (
|
||||||
<tr key={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">
|
<td className="w-12 h-8 bg-gray-100 border border-gray-300 text-xs text-gray-600 font-medium text-center">
|
||||||
{rowIndex + 1}
|
{rowIndex + 1}
|
||||||
@@ -464,93 +473,57 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
{row.map((cell, colIndex) => (
|
{row.map((cell, colIndex) => (
|
||||||
<td
|
<td
|
||||||
key={colIndex}
|
key={colIndex}
|
||||||
className={`border border-gray-300 relative ${
|
className={`border border-gray-200 ${
|
||||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) ? 'bg-blue-100' : ''
|
||||||
? ''
|
|
||||||
: 'hover:bg-gray-50'
|
|
||||||
} ${
|
} ${
|
||||||
cell.isModified ? 'bg-yellow-50' : ''
|
cell.isModified ? 'bg-yellow-50' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
width: widths[colIndex],
|
minWidth: `${widths[colIndex]}px`,
|
||||||
height: rowHeights[rowIndex],
|
width: `${widths[colIndex]}px`,
|
||||||
...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType
|
height: `${rowHeights[rowIndex]}px`,
|
||||||
? { boxShadow: isEditing
|
overflow: 'hidden',
|
||||||
? 'inset 0 0 0 2px #3b82f6, inset 0 0 0 4px rgba(59, 130, 246, 0.3)'
|
padding: 0,
|
||||||
: 'inset 0 0 0 2px #3b82f6'
|
position: 'relative'
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex && activeSheet === sheetType) {
|
if (activeSheet !== sheetType) setActiveSheet(sheetType);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeSheet !== sheetType) {
|
|
||||||
setActiveSheet(sheetType);
|
|
||||||
}
|
|
||||||
handleCellClick(rowIndex, colIndex);
|
handleCellClick(rowIndex, colIndex);
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)}
|
||||||
if (activeSheet !== sheetType) {
|
|
||||||
setActiveSheet(sheetType);
|
|
||||||
}
|
|
||||||
handleCellDoubleClick(rowIndex, colIndex);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex ? (
|
||||||
<input
|
<input
|
||||||
type="text"
|
ref={el => {
|
||||||
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]) {
|
if (inputRefs.current[sheetType][rowIndex]) {
|
||||||
inputRefs.current[sheetType][rowIndex][colIndex] = el;
|
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>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -562,70 +535,33 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({ templateData = {} }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-gray-50">
|
<div className="flex flex-col h-full space-y-4 p-4 bg-gray-100">
|
||||||
{/* Заголовок */}
|
|
||||||
<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-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="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-lg font-semibold mb-2">Отчет</h2>
|
||||||
<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')}
|
{renderSpreadsheet('report')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Правая панель - Вычисления */}
|
{/* Правая панель - Вычисления */}
|
||||||
<div className="w-1/2 flex flex-col">
|
<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-lg font-semibold mb-2">Расчеты</h2>
|
||||||
<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')}
|
{renderSpreadsheet('calculations')}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ElementType, TemplateElement } from '../../types/template';
|
|||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card, CardContent, CardHeader } from '../ui/card';
|
import { Card, CardContent, CardHeader } from '../ui/card';
|
||||||
import { Checkbox } from '../ui/checkbox';
|
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 { Input } from '../ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
|
||||||
@@ -231,9 +231,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between p-4 bg-white">
|
||||||
<h3 className="text-lg font-semibold">Конструктор элементов</h3>
|
<h3 className="text-lg font-semibold">Элементы формы</h3>
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size="sm" onClick={() => resetForm()}>
|
<Button size="sm" onClick={() => resetForm()}>
|
||||||
@@ -244,6 +244,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
<DialogContent className="sm:max-w-[600px]">
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Добавить элемент</DialogTitle>
|
<DialogTitle>Добавить элемент</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Создайте новый элемент формы для связи с ячейкой Excel
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ElementForm formData={formData} setFormData={setFormData} />
|
<ElementForm formData={formData} setFormData={setFormData} />
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
@@ -256,8 +259,23 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto pr-2 -mr-2">
|
<div className="flex-1 overflow-y-auto p-4 bg-white">
|
||||||
{elements.map(element => (
|
{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">
|
<Card key={element.id} className="mb-4">
|
||||||
<CardHeader className="flex flex-row items-center justify-between p-4">
|
<CardHeader className="flex flex-row items-center justify-between p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -277,6 +295,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
<DialogContent className="sm:max-w-[600px]">
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Измените настройки элемента формы
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ElementForm formData={formData} setFormData={setFormData} />
|
<ElementForm formData={formData} setFormData={setFormData} />
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
@@ -312,7 +333,8 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 React, { useRef, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { Template, TemplateElement } from '../../types/template';
|
import { Template } from '../../types/template';
|
||||||
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet';
|
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { ElementConstructor } from './ElementConstructor';
|
|
||||||
|
|
||||||
interface TemplateEditorProps {
|
interface TemplateEditorProps {
|
||||||
template: Template;
|
template: Template;
|
||||||
@@ -22,6 +22,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
const [editedTemplate, setEditedTemplate] = useState<Template>(template);
|
const [editedTemplate, setEditedTemplate] = useState<Template>(template);
|
||||||
const [excelData, setExcelData] = useState<Record<string, any>>(template.excelData || {});
|
const [excelData, setExcelData] = useState<Record<string, any>>(template.excelData || {});
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
@@ -70,36 +71,14 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
reader.readAsArrayBuffer(file);
|
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 = () => {
|
const handleSave = () => {
|
||||||
onSave(editedTemplate);
|
onSave(editedTemplate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateElements = () => {
|
||||||
|
navigate('/elements');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-gray-50">
|
<div className="h-screen flex flex-col bg-gray-50">
|
||||||
{/* Заголовок */}
|
{/* Заголовок */}
|
||||||
@@ -119,6 +98,14 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<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 variant="outline" onClick={onCancel}>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
@@ -130,10 +117,6 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Основной контент */}
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
{/* Загрузка Excel и таблицы */}
|
|
||||||
<div className="flex-1 flex flex-col">
|
|
||||||
{/* Панель загрузки Excel */}
|
{/* Панель загрузки Excel */}
|
||||||
<div className="bg-white border-b border-gray-200 p-4 flex-shrink-0">
|
<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 justify-between">
|
||||||
@@ -150,7 +133,10 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
<div className="text-sm text-gray-500">Файл не загружен</div>
|
<div className="text-sm text-gray-500">Файл не загружен</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Элементов создано: {editedTemplate.elements.length}
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -171,24 +157,12 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Таблицы */}
|
{/* Таблицы - теперь занимают всё оставшееся пространство */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<DualSpreadsheet
|
<DualSpreadsheet
|
||||||
templateData={{ Report: excelData }}
|
templateData={{ Report: excelData }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { FileText, Plus, Settings, Upload } from 'lucide-react';
|
import { FileText, Plus, Settings, Upload } from 'lucide-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useTemplateContext } from '../../contexts/TemplateContext';
|
||||||
import { Template } from '../../types/template';
|
import { Template } from '../../types/template';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { 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 { Input } from '../ui/input';
|
||||||
import { TemplateEditor } from './TemplateEditor';
|
import { TemplateEditor } from './TemplateEditor';
|
||||||
import { TemplateRenderer } from './TemplateRenderer';
|
import { TemplateRenderer } from './TemplateRenderer';
|
||||||
@@ -13,7 +14,7 @@ interface TemplateManagerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSelect }) => {
|
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 [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
@@ -32,7 +33,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
setTemplates(prev => [...prev, newTemplate]);
|
addTemplate(newTemplate);
|
||||||
setSelectedTemplate(newTemplate);
|
setSelectedTemplate(newTemplate);
|
||||||
setIsEditMode(true);
|
setIsEditMode(true);
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
@@ -41,14 +42,12 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateUpdate = (updatedTemplate: Template) => {
|
const handleTemplateUpdate = (updatedTemplate: Template) => {
|
||||||
setTemplates(prev => prev.map(t =>
|
updateTemplate(updatedTemplate);
|
||||||
t.id === updatedTemplate.id ? updatedTemplate : t
|
|
||||||
));
|
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateDelete = (templateId: string) => {
|
const handleTemplateDelete = (templateId: string) => {
|
||||||
setTemplates(prev => prev.filter(t => t.id !== templateId));
|
deleteTemplate(templateId);
|
||||||
if (selectedTemplate?.id === templateId) {
|
if (selectedTemplate?.id === templateId) {
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
setIsEditMode(false);
|
setIsEditMode(false);
|
||||||
@@ -94,8 +93,8 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
|||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Система протоколов</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Настройка шаблонов</h1>
|
||||||
<p className="text-gray-600 mt-2">Управление шаблонами и создание отчетов</p>
|
<p className="text-gray-600 mt-2">Создание и управление шаблонами протоколов</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
@@ -108,6 +107,9 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
|||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Создать новый шаблон</DialogTitle>
|
<DialogTitle>Создать новый шаблон</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Введите название и описание для нового шаблона.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -206,7 +208,7 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSele
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => handleTemplateSelect(template)}
|
onClick={() => handleTemplateSelect(template)}
|
||||||
>
|
>
|
||||||
Использовать
|
Предпросмотр
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ interface TemplateRendererProps {
|
|||||||
template: Template;
|
template: Template;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
|
hideSpreadsheet?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
|
export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
|
||||||
template,
|
template,
|
||||||
onBack,
|
onBack,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
hideSpreadsheet = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
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="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">
|
<div className="p-4 border-b border-gray-200 flex-shrink-0">
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Параметры отчета</h2>
|
<h2 className="text-lg font-semibold text-gray-800">Параметры отчета</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,21 +297,34 @@ export const TemplateRenderer: React.FC<TemplateRendererProps> = ({
|
|||||||
{errors[element.id] && (
|
{errors[element.id] && (
|
||||||
<p className="text-sm text-red-600">{errors[element.id]}</p>
|
<p className="text-sm text-red-600">{errors[element.id]}</p>
|
||||||
)}
|
)}
|
||||||
|
{!hideSpreadsheet && (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
Ячейка: {element.targetCell}
|
Ячейка: {element.targetCell}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Правая панель - таблица */}
|
{/* Правая панель - таблица (скрыта если hideSpreadsheet=true) */}
|
||||||
|
{!hideSpreadsheet && (
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<DualSpreadsheet
|
<DualSpreadsheet
|
||||||
templateData={{ Report: spreadsheetData }}
|
templateData={{ Report: spreadsheetData }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Скрытый DualSpreadsheet для работы в фоне */}
|
||||||
|
{hideSpreadsheet && (
|
||||||
|
<div className="hidden">
|
||||||
|
<DualSpreadsheet
|
||||||
|
templateData={{ Report: spreadsheetData }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
58
src/contexts/TemplateContext.tsx
Normal file
58
src/contexts/TemplateContext.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React, { createContext, ReactNode, useContext, useState } from 'react';
|
||||||
|
import { Template } from '../types/template';
|
||||||
|
|
||||||
|
interface TemplateContextType {
|
||||||
|
templates: Template[];
|
||||||
|
addTemplate: (template: Template) => void;
|
||||||
|
updateTemplate: (template: Template) => void;
|
||||||
|
deleteTemplate: (templateId: string) => void;
|
||||||
|
getTemplate: (templateId: string) => Template | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateContext = createContext<TemplateContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useTemplateContext = () => {
|
||||||
|
const context = useContext(TemplateContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTemplateContext must be used within a TemplateProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TemplateProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateProvider: React.FC<TemplateProviderProps> = ({ children }) => {
|
||||||
|
const [templates, setTemplates] = useState<Template[]>([]);
|
||||||
|
|
||||||
|
const addTemplate = (template: Template) => {
|
||||||
|
setTemplates(prev => [...prev, template]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTemplate = (updatedTemplate: Template) => {
|
||||||
|
setTemplates(prev => prev.map(t =>
|
||||||
|
t.id === updatedTemplate.id ? updatedTemplate : t
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTemplate = (templateId: string) => {
|
||||||
|
setTemplates(prev => prev.filter(t => t.id !== templateId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplate = (templateId: string) => {
|
||||||
|
return templates.find(t => t.id === templateId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TemplateContext.Provider value={{
|
||||||
|
templates,
|
||||||
|
addTemplate,
|
||||||
|
updateTemplate,
|
||||||
|
deleteTemplate,
|
||||||
|
getTemplate
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</TemplateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,8 +15,8 @@ interface UseSpreadsheetEngineProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSpreadsheetEngine({
|
export function useSpreadsheetEngine({
|
||||||
rows = 20,
|
rows = 50,
|
||||||
cols = 10,
|
cols = 20,
|
||||||
sheetName = 'Sheet1',
|
sheetName = 'Sheet1',
|
||||||
initialData
|
initialData
|
||||||
}: UseSpreadsheetEngineProps = {}) {
|
}: UseSpreadsheetEngineProps = {}) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export function convertFromReactData(reactData: string[][], sheetName: string =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Функция для конвертации данных из движка в React компонент
|
// Функция для конвертации данных из движка в React компонент
|
||||||
export function convertToReactData(wb: Workbook, sheetName: string = 'Sheet1', maxRows: number = 20, maxCols: number = 10): string[][] {
|
export function convertToReactData(wb: Workbook, sheetName: string = 'Sheet1', maxRows: number = 50, maxCols: number = 20): string[][] {
|
||||||
const sheet = wb.getSheet(sheetName);
|
const sheet = wb.getSheet(sheetName);
|
||||||
if (!sheet) {
|
if (!sheet) {
|
||||||
return Array(maxRows).fill(null).map(() => Array(maxCols).fill(''));
|
return Array(maxRows).fill(null).map(() => Array(maxCols).fill(''));
|
||||||
|
|||||||
3
src/pages/ElementsCreation/index.ts
Normal file
3
src/pages/ElementsCreation/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { ElementsCreation } from "./ui/Page";
|
||||||
|
|
||||||
|
export { ElementsCreation };
|
||||||
166
src/pages/ElementsCreation/ui/Page/Page.tsx
Normal file
166
src/pages/ElementsCreation/ui/Page/Page.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { Plus, Wrench } from 'lucide-react';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { useNavigate } 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 { Template, TemplateElement } from '../../../../types/template';
|
||||||
|
|
||||||
|
export const ElementsCreation: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { templates, updateTemplate } = useTemplateContext();
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||||
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleTemplateSelect = (template: Template) => {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleElementAdd = (element: TemplateElement) => {
|
||||||
|
if (!selectedTemplate) return;
|
||||||
|
|
||||||
|
const updatedTemplate = {
|
||||||
|
...selectedTemplate,
|
||||||
|
elements: [...selectedTemplate.elements, element],
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 handleBack = () => {
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedTemplate) {
|
||||||
|
return (
|
||||||
|
<div className="h-full bg-gray-50">
|
||||||
|
<div className="bg-white border-b border-gray-200 p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" onClick={handleBack}>
|
||||||
|
← Назад
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold text-gray-800">
|
||||||
|
Создание элементов: {selectedTemplate.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Создайте элементы формы для связи с ячейками шаблона
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => navigate('/templates')} variant="outline">
|
||||||
|
Перейти к шаблонам
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-blue-50">
|
||||||
|
<ElementConstructor
|
||||||
|
elements={selectedTemplate.elements}
|
||||||
|
onElementAdd={handleElementAdd}
|
||||||
|
onElementUpdate={handleElementUpdate}
|
||||||
|
onElementDelete={handleElementDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
</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="text-center py-12">
|
||||||
|
<Wrench className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Нет доступных шаблонов</h3>
|
||||||
|
<p className="text-gray-600 mb-6">Создайте шаблон для начала работы с элементами</p>
|
||||||
|
<Button onClick={() => navigate('/templates')}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Создать шаблон
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Card key={template.id} className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<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="text-xs text-gray-500 mt-2">
|
||||||
|
Обновлен: {template.updatedAt.toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleTemplateSelect(template)}
|
||||||
|
>
|
||||||
|
Создать элементы
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
src/pages/ElementsCreation/ui/Page/index.ts
Normal file
1
src/pages/ElementsCreation/ui/Page/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ElementsCreation } from './Page';
|
||||||
1
src/pages/ProtocolCreation/index.ts
Normal file
1
src/pages/ProtocolCreation/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ProtocolCreation } from './ui/Page/Page';
|
||||||
105
src/pages/ProtocolCreation/ui/Page/Page.tsx
Normal file
105
src/pages/ProtocolCreation/ui/Page/Page.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { FileText, Plus } from 'lucide-react';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { TemplateRenderer } from '../../../../components/TemplateManager/TemplateRenderer';
|
||||||
|
import { Button } from '../../../../components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../../components/ui/card';
|
||||||
|
import { useTemplateContext } from '../../../../contexts/TemplateContext';
|
||||||
|
import { Template } from '../../../../types/template';
|
||||||
|
|
||||||
|
export const ProtocolCreation: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
</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="text-center py-12">
|
||||||
|
<FileText className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Нет доступных шаблонов</h3>
|
||||||
|
<p className="text-gray-600 mb-6">Создайте шаблон протокола для начала работы</p>
|
||||||
|
<Button onClick={() => navigate('/templates')}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Создать шаблон
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Card key={template.id} className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<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="text-xs text-gray-500 mt-2">
|
||||||
|
Создан: {template.createdAt.toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleTemplateSelect(template)}
|
||||||
|
disabled={!template.excelFile}
|
||||||
|
>
|
||||||
|
{template.excelFile ? 'Создать протокол' : 'Шаблон не настроен'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
src/pages/TemplateSetup/index.ts
Normal file
1
src/pages/TemplateSetup/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { TemplateSetup } from './ui/Page/Page';
|
||||||
6
src/pages/TemplateSetup/ui/Page/Page.tsx
Normal file
6
src/pages/TemplateSetup/ui/Page/Page.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { TemplateManager } from '../../../../components/TemplateManager/TemplateManager';
|
||||||
|
|
||||||
|
export const TemplateSetup: FC = () => {
|
||||||
|
return <TemplateManager />;
|
||||||
|
};
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { ElementsCreation } from "./ElementsCreation";
|
||||||
import { Home } from "./Home";
|
import { Home } from "./Home";
|
||||||
import { NoMatch } from "./NoMatch";
|
import { NoMatch } from "./NoMatch";
|
||||||
|
import { ProtocolCreation } from "./ProtocolCreation";
|
||||||
|
import { TemplateSetup } from "./TemplateSetup";
|
||||||
|
|
||||||
|
export { ElementsCreation, Home, NoMatch, ProtocolCreation, TemplateSetup };
|
||||||
|
|
||||||
export { Home, NoMatch };
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Типы элементов интерфейса
|
ц// Типы элементов интерфейса
|
||||||
export type ElementType = 'text' | 'select' | 'radio' | 'checkbox' | 'number' | 'textarea' | 'date';
|
export type ElementType = 'text' | 'select' | 'radio' | 'checkbox' | 'number' | 'textarea' | 'date';
|
||||||
|
|
||||||
export interface ElementOption {
|
export interface ElementOption {
|
||||||
|
|||||||
Reference in New Issue
Block a user