система инит

This commit is contained in:
Artyom Tsyrulnikov
2025-07-15 06:42:04 +03:00
parent 2100fcd249
commit 6bd5bba15a
22 changed files with 7273 additions and 257 deletions

4631
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,31 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@types/react-grid-layout": "^1.3.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.525.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"redux": "^4.2.1" "react-grid-layout": "^1.5.2",
"redux": "^4.2.1",
"tailwind-merge": "^3.3.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.37", "@types/react": "^18.0.37",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0", "@typescript-eslint/parser": "^5.59.0",

View File

@@ -1,18 +1,11 @@
import { FC } from "react"; import { FC } from "react";
import { Route, Routes } from "react-router-dom"; import { TemplateManager } from "../components/TemplateManager/TemplateManager";
import { Layout } from "@/app/Layout";
import { Home, NoMatch } from "@/pages";
const App: FC = () => { const App: FC = () => {
return ( return (
<> <div className="min-h-screen bg-background">
<Routes> <TemplateManager />
<Route path="/" element={<Layout />}> </div>
<Route index element={<Home />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</>
); );
}; };

View File

@@ -1,3 +1,59 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -97,6 +97,18 @@ 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));
}
}
});
} }
}, [templateData]); }, [templateData]);

View File

@@ -0,0 +1,319 @@
import { Calendar, CheckSquare, ChevronDown, Edit, FileText, Hash, Plus, Trash2, Type } from 'lucide-react';
import React, { useState } from 'react';
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 { Input } from '../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
interface ElementFormProps {
formData: Partial<TemplateElement>;
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>;
}
const ELEMENT_TYPES: Array<{ type: ElementType; label: string; icon: React.ReactNode }> = [
{ type: 'text', label: 'Текстовое поле', icon: <Type className="h-4 w-4" /> },
{ type: 'textarea', label: 'Многострочный текст', icon: <FileText className="h-4 w-4" /> },
{ type: 'number', label: 'Число', icon: <Hash className="h-4 w-4" /> },
{ type: 'select', label: 'Выпадающий список', icon: <ChevronDown className="h-4 w-4" /> },
{ type: 'radio', label: 'Радиокнопки', icon: <CheckSquare className="h-4 w-4" /> },
{ type: 'checkbox', label: 'Чекбокс', icon: <CheckSquare className="h-4 w-4" /> },
{ type: 'date', label: 'Дата', icon: <Calendar className="h-4 w-4" /> },
];
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
const handleAddOption = () => {
setFormData(prev => ({
...prev,
options: [...(prev.options || []), { value: '', label: '' }],
}));
};
const handleUpdateOption = (index: number, field: 'value' | 'label', value: string) => {
setFormData(prev => ({
...prev,
options: prev.options?.map((option, i) => (i === index ? { ...option, [field]: value } : option)),
}));
};
const handleRemoveOption = (index: number) => {
setFormData(prev => ({
...prev,
options: prev.options?.filter((_, i) => i !== index),
}));
};
const needsOptions = formData.type === 'select' || formData.type === 'radio';
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Тип элемента</label>
<Select
value={formData.type}
onValueChange={value => setFormData(prev => ({ ...prev, type: value as ElementType }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ELEMENT_TYPES.map(({ type, label, icon }) => (
<SelectItem key={type} value={type}>
<div className="flex items-center gap-2">
{icon}
{label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Ячейка отчета</label>
<Input
placeholder="A1, B5, C10..."
value={formData.targetCell}
onChange={e => setFormData(prev => ({ ...prev, targetCell: e.target.value }))}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Имя поля</label>
<Input
placeholder="field_name"
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Подпись</label>
<Input
placeholder="Название поля"
value={formData.label}
onChange={e => setFormData(prev => ({ ...prev, label: e.target.value }))}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Подсказка</label>
<Input
placeholder="Введите значение..."
value={formData.placeholder}
onChange={e => setFormData(prev => ({ ...prev, placeholder: e.target.value }))}
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="required"
checked={formData.required}
onCheckedChange={checked => setFormData(prev => ({ ...prev, required: !!checked }))}
/>
<label htmlFor="required" className="text-sm font-medium">
Обязательное поле
</label>
</div>
{needsOptions && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Варианты</label>
<Button variant="outline" size="sm" onClick={handleAddOption}>
<Plus className="h-4 w-4 mr-1" />
Добавить
</Button>
</div>
<div className="space-y-2 max-h-32 overflow-y-auto">
{formData.options?.map((option, index) => (
<div key={index} className="flex gap-2">
<Input
placeholder="Значение"
value={option.value}
onChange={e => handleUpdateOption(index, 'value', e.target.value)}
/>
<Input
placeholder="Подпись"
value={option.label}
onChange={e => handleUpdateOption(index, 'label', e.target.value)}
/>
<Button variant="outline" size="icon" onClick={() => handleRemoveOption(index)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
</div>
);
};
interface ElementConstructorProps {
elements: TemplateElement[];
onElementAdd: (element: TemplateElement) => void;
onElementUpdate: (elementId: string, updates: Partial<TemplateElement>) => void;
onElementDelete: (elementId: string) => void;
}
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
elements,
onElementAdd,
onElementUpdate,
onElementDelete,
}) => {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [editingElement, setEditingElement] = useState<TemplateElement | null>(null);
const [formData, setFormData] = useState<Partial<TemplateElement>>({
type: 'text',
name: '',
label: '',
targetCell: '',
placeholder: '',
required: false,
options: [],
});
const resetForm = () => {
setFormData({
type: 'text',
name: '',
label: '',
targetCell: '',
placeholder: '',
required: false,
options: [],
});
};
const handleAddElement = () => {
if (!formData.name || !formData.label || !formData.targetCell) return;
const newElement: TemplateElement = {
id: Date.now().toString(),
type: formData.type as ElementType,
name: formData.name,
label: formData.label,
targetCell: formData.targetCell,
placeholder: formData.placeholder,
required: formData.required,
options: formData.options,
};
onElementAdd(newElement);
resetForm();
setIsAddDialogOpen(false);
};
const handleEditElement = (element: TemplateElement) => {
setEditingElement(element);
setFormData(element);
};
const handleUpdateElement = () => {
if (!editingElement || !formData.name || !formData.label || !formData.targetCell) return;
onElementUpdate(editingElement.id, formData);
setEditingElement(null);
resetForm();
};
const handleCancelEdit = () => {
setEditingElement(null);
resetForm();
};
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>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" onClick={() => resetForm()}>
<Plus className="h-4 w-4 mr-2" />
Добавить элемент
</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={() => setIsAddDialogOpen(false)}>
Отмена
</Button>
<Button onClick={handleAddElement}>Добавить</Button>
</div>
</DialogContent>
</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}>
Отмена
</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>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,194 @@
import { ArrowLeft, Save, Upload } from 'lucide-react';
import React, { useRef, useState } from 'react';
import * as XLSX from 'xlsx';
import { Template, TemplateElement } from '../../types/template';
import DualSpreadsheet from '../DualSpreadsheet/DualSpreadsheet';
import { Button } from '../ui/button';
import { ElementConstructor } from './ElementConstructor';
interface TemplateEditorProps {
template: Template;
onSave: (template: Template) => void;
onCancel: () => void;
onBack: () => void;
}
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
template,
onSave,
onCancel,
onBack,
}) => {
const [editedTemplate, setEditedTemplate] = useState<Template>(template);
const [excelData, setExcelData] = useState<Record<string, any>>(template.excelData || {});
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target?.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
// Читаем первый лист
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// Преобразуем в формат для нашей системы
const formattedData: Record<string, any> = {};
(jsonData as any[][]).forEach((row, rowIndex) => {
row.forEach((cell, colIndex) => {
if (cell !== undefined && cell !== null && cell !== '') {
const cellName = String.fromCharCode(65 + colIndex) + (rowIndex + 1);
formattedData[cellName] = cell;
}
});
});
setExcelData(formattedData);
setEditedTemplate(prev => ({
...prev,
excelFile: file,
excelData: formattedData,
updatedAt: new Date(),
}));
// Принудительно обновляем данные в таблице
setTimeout(() => {
setExcelData({...formattedData});
}, 100);
} catch (error) {
console.error('Ошибка при чтении Excel файла:', error);
alert('Ошибка при чтении Excel файла');
}
};
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);
};
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">
Редактирование шаблона: {editedTemplate.name}
</h1>
<p className="text-sm text-gray-600 mt-1">
Настройте Excel шаблон и создайте элементы интерфейса
</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={onCancel}>
Отмена
</Button>
<Button onClick={handleSave} className="flex items-center gap-2">
<Save className="h-4 w-4" />
Сохранить
</Button>
</div>
</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>
</div>
</div>
{/* Таблицы */}
<div className="flex-1 min-h-0">
<DualSpreadsheet
templateData={{ Report: excelData }}
/>
</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>
);
};

View File

@@ -0,0 +1,229 @@
import { FileText, Plus, Settings, Upload } from 'lucide-react';
import React, { useState } from 'react';
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 { Input } from '../ui/input';
import { TemplateEditor } from './TemplateEditor';
import { TemplateRenderer } from './TemplateRenderer';
interface TemplateManagerProps {
onTemplateSelect?: (template: Template) => void;
}
export const TemplateManager: React.FC<TemplateManagerProps> = ({ onTemplateSelect }) => {
const [templates, setTemplates] = useState<Template[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [newTemplateName, setNewTemplateName] = useState('');
const [newTemplateDescription, setNewTemplateDescription] = useState('');
const handleCreateTemplate = () => {
if (!newTemplateName.trim()) return;
const newTemplate: Template = {
id: Date.now().toString(),
name: newTemplateName.trim(),
description: newTemplateDescription.trim(),
elements: [],
createdAt: new Date(),
updatedAt: new Date(),
};
setTemplates(prev => [...prev, newTemplate]);
setSelectedTemplate(newTemplate);
setIsEditMode(true);
setIsCreateDialogOpen(false);
setNewTemplateName('');
setNewTemplateDescription('');
};
const handleTemplateUpdate = (updatedTemplate: Template) => {
setTemplates(prev => prev.map(t =>
t.id === updatedTemplate.id ? updatedTemplate : t
));
setSelectedTemplate(updatedTemplate);
};
const handleTemplateDelete = (templateId: string) => {
setTemplates(prev => prev.filter(t => t.id !== templateId));
if (selectedTemplate?.id === templateId) {
setSelectedTemplate(null);
setIsEditMode(false);
}
};
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template);
setIsEditMode(false);
onTemplateSelect?.(template);
};
const handleEditTemplate = (template: Template) => {
setSelectedTemplate(template);
setIsEditMode(true);
};
if (selectedTemplate && !isEditMode) {
return (
<TemplateRenderer
template={selectedTemplate}
onBack={() => setSelectedTemplate(null)}
onEdit={() => setIsEditMode(true)}
/>
);
}
if (selectedTemplate && isEditMode) {
return (
<TemplateEditor
template={selectedTemplate}
onSave={handleTemplateUpdate}
onCancel={() => setIsEditMode(false)}
onBack={() => {
setSelectedTemplate(null);
setIsEditMode(false);
}}
/>
);
}
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>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Создать шаблон
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Создать новый шаблон</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Название шаблона</label>
<Input
placeholder="Введите название шаблона"
value={newTemplateName}
onChange={(e) => setNewTemplateName(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Описание (опционально)</label>
<Input
placeholder="Введите описание шаблона"
value={newTemplateDescription}
onChange={(e) => setNewTemplateDescription(e.target.value)}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Отмена
</Button>
<Button
onClick={handleCreateTemplate}
disabled={!newTemplateName.trim()}
>
Создать
</Button>
</div>
</DialogContent>
</Dialog>
</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={() => setIsCreateDialogOpen(true)}>
<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>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg">{template.name}</CardTitle>
{template.description && (
<CardDescription className="mt-1">
{template.description}
</CardDescription>
)}
</div>
<div className="flex gap-1 ml-2">
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleEditTemplate(template);
}}
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center gap-2">
<Upload className="h-4 w-4" />
<span>
{template.excelFile ? 'Excel файл загружен' : 'Excel файл не загружен'}
</span>
</div>
<div className="flex items-center gap-2">
<Settings 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="flex gap-2 mt-4">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleTemplateSelect(template)}
>
Использовать
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleEditTemplate(template);
}}
>
Настроить
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
};

View File

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

View File

@@ -0,0 +1,40 @@
import * as React from "react"
import { cn } from "../../lib/utils"
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg' | 'icon'
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
return (
<button
className={cn(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
'border border-input bg-background hover:bg-accent hover:text-accent-foreground': variant === 'outline',
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
'text-primary underline-offset-4 hover:underline': variant === 'link',
},
{
'h-10 px-4 py-2': size === 'default',
'h-9 rounded-md px-3': size === 'sm',
'h-11 rounded-md px-8': size === 'lg',
'h-10 w-10': size === 'icon',
},
className
)}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button }

View File

@@ -0,0 +1,78 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }

View File

@@ -0,0 +1,27 @@
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import { cn } from "../../lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,110 @@
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import { cn } from "../../lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger
}

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "../../lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,41 @@
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import * as React from "react"
import { cn } from "../../lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,148 @@
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react"
import { cn } from "../../lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue
}

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "../../lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

46
src/types/template.ts Normal file
View File

@@ -0,0 +1,46 @@
// Типы элементов интерфейса
export type ElementType = 'text' | 'select' | 'radio' | 'checkbox' | 'number' | 'textarea' | 'date';
export interface ElementOption {
value: string;
label: string;
}
export interface TemplateElement {
id: string;
type: ElementType;
name: string;
label: string;
targetCell: string; // например "A1", "B5"
options?: ElementOption[]; // для select и radio
required?: boolean;
defaultValue?: string;
placeholder?: string;
}
// Типы шаблонов
export interface Template {
id: string;
name: string;
description?: string;
excelFile?: File;
excelData?: Record<string, any>; // данные из Excel
elements: TemplateElement[];
createdAt: Date;
updatedAt: Date;
}
// Типы для работы с данными
export interface TemplateData {
templateId: string;
values: Record<string, any>; // elementId -> value
calculatedCells?: Record<string, any>; // cellName -> value
}
// Типы для экспорта
export interface ExportData {
templateId: string;
templateName: string;
excelFile: File;
cellUpdates: Record<string, any>; // cellName -> value
}

View File

@@ -2,7 +2,48 @@
export default { export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { theme: {
extend: {}, extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
}, },
plugins: [], plugins: [],
}; };

1
temp Normal file
View File

@@ -0,0 +1 @@
Так теперь продолжаем делать нашу систему. У нас должно быть изначально меню, где мы можем создавать шаблоны на создание протоколов. Мы даем в этом меню ему имя. Далее мы можем создавать шаблон, а это продгрузка excel файла, который будет шаблон (заполняться строчки левого меню отчета). И можем проводить рассчеты справа и изменять ячейки отчета. Также ниже от этих двух таблиц должны быть конструктор элементов которые тоже привяжутся к имени этого шаблона, там мы сможем создавать просто элементы который будут видны в интерфейсы, давать им имена и ячейку в которую будут вноситься данные при выборе в этих элементах (это будут например, элемент ввода текста, кнопки, радиокнопки, выпадающий список и тп). Тут важно интерфейс общий сделать чтобы удобно можно было добавлять новый вид элементов. Вот это как бы меню конструктор для создания отчетов, потом когда мы выбираем этот шаблон из списка, мы видим все наши элементы, мы туда вводим инофрмацию, у нас посчитывается ячейки что мы указали изменяемыми в левом отчете, и отправляется информация на бэк что за excel файл отчета, какие ячейки с каким значением туда надо вставить.

1151
yarn.lock

File diff suppressed because it is too large Load Diff