From fb1d4ae7ad9445a8b59b71781f3fbb4d70e5a40a Mon Sep 17 00:00:00 2001 From: kikimor Date: Sun, 25 Jan 2026 09:58:55 +0200 Subject: [PATCH] report editor with text field, lines, frames, and table draw. multiselect etc. --- .claude/settings.local.json | 5 +- .gitignore | 4 + frontend/src/App.js | 6 +- frontend/src/components/Header.css | 27 + frontend/src/components/Header.js | 9 + .../ReportEditor/CharacterPalette.css | 150 ++++++ .../ReportEditor/CharacterPalette.js | 109 ++++ .../components/ReportEditor/ConfigPanel.css | 203 +++++++ .../components/ReportEditor/ConfigPanel.js | 154 ++++++ .../components/ReportEditor/DataContext.js | 68 +++ .../components/ReportEditor/EditorCanvas.css | 75 +++ .../components/ReportEditor/EditorCanvas.js | 496 ++++++++++++++++++ .../components/ReportEditor/FrameElement.js | 180 +++++++ .../components/ReportEditor/HorizontalLine.js | 139 +++++ .../components/ReportEditor/ReportEditor.css | 6 + .../components/ReportEditor/ReportEditor.js | 194 +++++++ .../components/ReportEditor/ResizeHandles.js | 249 +++++++++ .../components/ReportEditor/SymbolElement.js | 98 ++++ .../src/components/ReportEditor/TextField.js | 197 +++++++ .../src/components/ReportEditor/Toolbar.css | 52 ++ .../src/components/ReportEditor/Toolbar.js | 100 ++++ .../components/ReportEditor/VerticalLine.js | 141 +++++ .../src/components/ReportEditor/elements.css | 228 ++++++++ .../ReportEditor/utils/bindingParser.js | 88 ++++ .../ReportEditor/utils/crossroadDetector.js | 186 +++++++ frontend/tmpclaude-590b-cwd | 1 - 26 files changed, 3162 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ReportEditor/CharacterPalette.css create mode 100644 frontend/src/components/ReportEditor/CharacterPalette.js create mode 100644 frontend/src/components/ReportEditor/ConfigPanel.css create mode 100644 frontend/src/components/ReportEditor/ConfigPanel.js create mode 100644 frontend/src/components/ReportEditor/DataContext.js create mode 100644 frontend/src/components/ReportEditor/EditorCanvas.css create mode 100644 frontend/src/components/ReportEditor/EditorCanvas.js create mode 100644 frontend/src/components/ReportEditor/FrameElement.js create mode 100644 frontend/src/components/ReportEditor/HorizontalLine.js create mode 100644 frontend/src/components/ReportEditor/ReportEditor.css create mode 100644 frontend/src/components/ReportEditor/ReportEditor.js create mode 100644 frontend/src/components/ReportEditor/ResizeHandles.js create mode 100644 frontend/src/components/ReportEditor/SymbolElement.js create mode 100644 frontend/src/components/ReportEditor/TextField.js create mode 100644 frontend/src/components/ReportEditor/Toolbar.css create mode 100644 frontend/src/components/ReportEditor/Toolbar.js create mode 100644 frontend/src/components/ReportEditor/VerticalLine.js create mode 100644 frontend/src/components/ReportEditor/elements.css create mode 100644 frontend/src/components/ReportEditor/utils/bindingParser.js create mode 100644 frontend/src/components/ReportEditor/utils/crossroadDetector.js delete mode 100644 frontend/tmpclaude-590b-cwd diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 8029292..a3bbff7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,10 @@ { "permissions": { "allow": [ - "Bash(python manage.py migrate:*)" + "Bash(python manage.py migrate:*)", + "Bash(tree:*)", + "Bash(dir /b \"c:\\\\dev_projects\\\\ScalesApp\\\\frontend\\\\src\\\\contexts\")", + "Bash(ls:*)" ] } } diff --git a/.gitignore b/.gitignore index 50b3f7c..8318b49 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ yarn-error.log* # Environment .env .env.local + +# Claude Code temporary files +tmpclaude-* +nul diff --git a/frontend/src/App.js b/frontend/src/App.js index f736c35..8e94601 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -4,6 +4,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'; // import ProtectedRoute from './components/ProtectedRoute'; import Login from './components/Users/Login'; import Main from './components/Main'; +import ReportEditor from './components/ReportEditor/ReportEditor'; import './App.css'; import { NomenclatureProvider } from './contexts/NomenclatureContext'; @@ -54,7 +55,10 @@ function AppContent() { return ( -
+ + } /> + } /> + ); } diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css index da383b2..d3b0407 100644 --- a/frontend/src/components/Header.css +++ b/frontend/src/components/Header.css @@ -17,12 +17,39 @@ align-items: center; } +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + .header-left h1 { margin: 0; font-size: 24px; font-weight: 600; } +.nav-button { + width: 42px; + height: 42px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + font-size: 20px; + cursor: pointer; + transition: background 0.2s, transform 0.2s; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.nav-button:hover { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.05); +} + .header-right { display: flex; align-items: center; diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 9417f7e..a8a24eb 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -1,10 +1,12 @@ import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import ChangePasswordOverlay from './Users/ChangePasswordOverlay'; import './Header.css'; function Header() { const { currentUser, logout } = useAuth(); + const navigate = useNavigate(); const [showPasswordOverlay, setShowPasswordOverlay] = useState(false); const getInitials = (user) => { @@ -23,6 +25,13 @@ function Header() {
+

ScalesApp - Real-time Data Monitor

diff --git a/frontend/src/components/ReportEditor/CharacterPalette.css b/frontend/src/components/ReportEditor/CharacterPalette.css new file mode 100644 index 0000000..35bf34d --- /dev/null +++ b/frontend/src/components/ReportEditor/CharacterPalette.css @@ -0,0 +1,150 @@ +.character-palette { + position: fixed; + right: 0; + top: 0; + width: 300px; + height: 100vh; + background: white; + border-left: 1px solid #ccc; + box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + overflow-y: auto; + z-index: 1000; + display: flex; + flex-direction: column; +} + +.palette-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + border-bottom: 1px solid #e0e0e0; + background: #f9f9f9; + position: sticky; + top: 0; + z-index: 10; +} + +.palette-header h3 { + margin: 0; + font-size: 16px; + color: #333; +} + +.close-button { + background: transparent; + border: none; + font-size: 20px; + cursor: pointer; + color: #666; + padding: 5px 10px; + line-height: 1; +} + +.close-button:hover { + color: #333; + background: rgba(0, 0, 0, 0.05); + border-radius: 3px; +} + +.palette-content { + flex: 1; + overflow-y: auto; + padding: 10px; +} + +.palette-category { + margin-bottom: 10px; +} + +.category-header { + display: flex; + align-items: center; + padding: 10px; + background: #f5f5f5; + cursor: pointer; + border-radius: 4px; + user-select: none; + transition: background 0.2s; +} + +.category-header:hover { + background: #e8e8e8; +} + +.category-header.expanded { + background: #667eea; + color: white; +} + +.category-icon { + margin-right: 8px; + font-size: 12px; + width: 12px; + display: inline-block; +} + +.category-name { + font-weight: 500; + font-size: 14px; +} + +.character-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); + gap: 5px; + padding: 10px; + background: #fafafa; + border-radius: 0 0 4px 4px; +} + +.char-button { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 1px solid #ddd; + background: white; + cursor: pointer; + font-size: 24px; + font-family: 'Courier New', 'Consolas', monospace; + transition: all 0.2s; + border-radius: 3px; + user-select: none; +} + +.char-button:hover { + border-color: #667eea; + background: #f0f0ff; + transform: scale(1.1); +} + +.char-button.active { + background: #667eea; + color: white; + border-color: #667eea; + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +/* Scrollbar styling */ +.character-palette::-webkit-scrollbar, +.palette-content::-webkit-scrollbar { + width: 8px; +} + +.character-palette::-webkit-scrollbar-track, +.palette-content::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.character-palette::-webkit-scrollbar-thumb, +.palette-content::-webkit-scrollbar-thumb { + background: #c0c0c0; + border-radius: 4px; +} + +.character-palette::-webkit-scrollbar-thumb:hover, +.palette-content::-webkit-scrollbar-thumb:hover { + background: #a0a0a0; +} diff --git a/frontend/src/components/ReportEditor/CharacterPalette.js b/frontend/src/components/ReportEditor/CharacterPalette.js new file mode 100644 index 0000000..d19e743 --- /dev/null +++ b/frontend/src/components/ReportEditor/CharacterPalette.js @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import './CharacterPalette.css'; + +const SYMBOL_PALETTE = { + 'Single Lines': { + horizontal: '─', + vertical: '│', + topLeft: '┌', + topRight: '┐', + bottomLeft: '└', + bottomRight: '┘', + cross: '┼', + tTop: '┴', + tBottom: '┬', + tLeft: '┤', + tRight: '├' + }, + 'Double Lines': { + horizontal: '═', + vertical: '║', + topLeft: '╔', + topRight: '╗', + bottomLeft: '╚', + bottomRight: '╝', + cross: '╬', + tTop: '╩', + tBottom: '╦', + tLeft: '╣', + tRight: '╠' + }, + 'Mixed Junctions': { + crossSingleH: '╫', + crossSingleV: '╪', + doubleDownSingleH: '╤', + doubleUpSingleH: '╧', + doubleRightSingleV: '╟', + doubleLeftSingleV: '╢' + }, + 'Rounded Corners': { + topLeft: '╭', + topRight: '╮', + bottomLeft: '╰', + bottomRight: '╯' + }, + 'Heavy Lines': { + horizontal: '━', + vertical: '┃', + topLeft: '┏', + topRight: '┓', + bottomLeft: '┗', + bottomRight: '┛' + }, + 'Common Symbols': { + space: ' ', + bullet: '•', + dash: '–', + equals: '=', + underscore: '_', + hash: '#' + } +}; + +function CharacterPalette({ selectedChar, onSelectChar, onClose }) { + const [expandedCategory, setExpandedCategory] = useState('Single Lines'); + + const toggleCategory = (category) => { + setExpandedCategory(prev => prev === category ? null : category); + }; + + return ( +
+
+

Character Palette

+ +
+ +
+ {Object.entries(SYMBOL_PALETTE).map(([category, chars]) => ( +
+
toggleCategory(category)} + > + {expandedCategory === category ? '▼' : '▶'} + {category} +
+ + {expandedCategory === category && ( +
+ {Object.entries(chars).map(([name, char]) => ( +
onSelectChar(char)} + title={name.replace(/([A-Z])/g, ' $1').trim()} + > + {char} +
+ ))} +
+ )} +
+ ))} +
+
+ ); +} + +export default CharacterPalette; diff --git a/frontend/src/components/ReportEditor/ConfigPanel.css b/frontend/src/components/ReportEditor/ConfigPanel.css new file mode 100644 index 0000000..c7ed8ca --- /dev/null +++ b/frontend/src/components/ReportEditor/ConfigPanel.css @@ -0,0 +1,203 @@ +.config-panel-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.config-panel { + background: white; + border-radius: 8px; + width: 90%; + max-width: 600px; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.config-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #e0e0e0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 8px 8px 0 0; +} + +.config-panel-header h2 { + margin: 0; + font-size: 20px; + font-weight: 600; +} + +.close-button { + background: none; + border: none; + color: white; + font-size: 32px; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background 0.2s; +} + +.close-button:hover { + background: rgba(255, 255, 255, 0.2); +} + +.config-panel-content { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.config-section { + margin-bottom: 24px; +} + +.config-section h3 { + margin: 0 0 12px 0; + font-size: 16px; + font-weight: 600; + color: #333; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.input-group { + display: flex; + gap: 8px; +} + +.config-input { + flex: 1; + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + font-family: 'Courier New', monospace; +} + +.config-input:focus { + outline: none; + border-color: #667eea; +} + +.config-input:disabled { + background: #f5f5f5; + cursor: not-allowed; +} + +.config-button { + padding: 10px 16px; + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.config-button:hover:not(:disabled) { + background: #e0e0e0; + transform: translateY(-1px); +} + +.config-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.config-button.primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: transparent; +} + +.config-button.primary:hover:not(:disabled) { + opacity: 0.9; +} + +.config-button.small { + padding: 6px 12px; + font-size: 12px; +} + +.config-error { + margin-top: 8px; + padding: 12px; + background: #fee; + border: 1px solid #fcc; + border-radius: 6px; + color: #c33; + font-size: 14px; +} + +.config-info { + padding: 16px; + background: #f0f8ff; + border: 1px solid #d0e8ff; + border-radius: 6px; + color: #0066cc; + font-size: 14px; + text-align: center; +} + +.manual-input-section { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.manual-data-input { + padding: 10px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + font-family: 'Courier New', monospace; + resize: vertical; + min-height: 120px; +} + +.manual-data-input:focus { + outline: none; + border-color: #667eea; +} + +.data-preview { + max-height: 300px; + overflow-y: auto; + padding: 12px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 6px; +} + +.data-preview pre { + margin: 0; + font-size: 12px; + font-family: 'Courier New', monospace; + white-space: pre-wrap; + word-wrap: break-word; +} diff --git a/frontend/src/components/ReportEditor/ConfigPanel.js b/frontend/src/components/ReportEditor/ConfigPanel.js new file mode 100644 index 0000000..02e5fc6 --- /dev/null +++ b/frontend/src/components/ReportEditor/ConfigPanel.js @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { useReportData } from './DataContext'; +import './ConfigPanel.css'; + +function ConfigPanel({ apiEndpoint, onApiEndpointChange, isOpen, onClose }) { + const { reportData, isLoading, error, fetchData, setData, clearData } = useReportData(); + const [localEndpoint, setLocalEndpoint] = useState(apiEndpoint || ''); + const [manualDataInput, setManualDataInput] = useState(''); + const [showManualInput, setShowManualInput] = useState(false); + + const handleFetch = async () => { + if (localEndpoint.trim()) { + onApiEndpointChange(localEndpoint); + await fetchData(localEndpoint); + } + }; + + const handleSetManualData = () => { + const trimmedInput = manualDataInput.trim(); + + if (!trimmedInput) { + alert('Please enter some JSON data first'); + return; + } + + try { + const data = JSON.parse(trimmedInput); + setData(data); + setShowManualInput(false); + setManualDataInput(''); + } catch (err) { + alert('Invalid JSON: ' + err.message); + } + }; + + const handleClear = () => { + clearData(); + setLocalEndpoint(''); + onApiEndpointChange(''); + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+

Report Configuration

+ +
+ +
+ {/* API Endpoint Section */} +
+

API Endpoint

+
+ setLocalEndpoint(e.target.value)} + disabled={isLoading} + /> + +
+ + {error && ( +
+ Error: {error} +
+ )} +
+ + {/* Manual Data Input Section */} +
+ + + {showManualInput && ( +
+