added vehicles and vehicles extra, dynamic tables for entities, dynamic dropdowns with overlay for selecting and manage data

This commit is contained in:
2026-02-10 17:32:33 +02:00
parent ed35a90cc0
commit 6a42099169
60 changed files with 4845 additions and 81 deletions
@@ -10,3 +10,146 @@
flex: 1;
overflow: hidden;
}
/* Load Report Dialog */
.load-dialog-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;
}
.load-dialog {
background: white;
border-radius: 8px;
width: 500px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.load-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.load-dialog-header h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.load-dialog-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
line-height: 1;
}
.load-dialog-close:hover {
color: #333;
}
.load-dialog-actions {
padding: 12px 20px;
border-bottom: 1px solid #eee;
}
.load-dialog-new-btn {
padding: 8px 16px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.load-dialog-new-btn:hover {
background: #5a6fd6;
}
.load-dialog-content {
flex: 1;
overflow-y: auto;
padding: 12px 0;
}
.load-dialog-loading,
.load-dialog-empty {
padding: 40px 20px;
text-align: center;
color: #999;
}
.load-dialog-list {
list-style: none;
margin: 0;
padding: 0;
}
.load-dialog-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.load-dialog-item:hover {
background: #f8f8f8;
}
.load-dialog-item--current {
background: #e8f0ff;
}
.load-dialog-item--current:hover {
background: #dde8ff;
}
.load-dialog-item-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.load-dialog-item-name {
font-weight: 500;
color: #333;
}
.load-dialog-item-date {
font-size: 12px;
color: #999;
}
.load-dialog-item-delete {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 4px 8px;
opacity: 0.5;
transition: opacity 0.2s;
}
.load-dialog-item-delete:hover {
opacity: 1;
}
@@ -6,6 +6,7 @@ import EditorCanvas from './EditorCanvas';
import ConfigPanel from './ConfigPanel';
import CharacterPalette from './CharacterPalette';
import ObjectInspector from './ObjectInspector';
import api from '../../services/api';
import './ReportEditor.css';
function ReportEditorContent() {
@@ -25,6 +26,13 @@ function ReportEditorContent() {
const [selectedChar, setSelectedChar] = useState('─');
const [showCharacterPalette, setShowCharacterPalette] = useState(false);
// Save/Load state
const [reportId, setReportId] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [showLoadDialog, setShowLoadDialog] = useState(false);
const [savedReports, setSavedReports] = useState([]);
const [loadingReports, setLoadingReports] = useState(false);
const handleAddElement = (element) => {
setReport(prev => {
// Check if the new element is positioned inside a band
@@ -178,6 +186,112 @@ function ReportEditorContent() {
}));
};
const handleReportNameChange = (name) => {
setReport(prev => ({ ...prev, name }));
};
// Save report to backend
const handleSave = async () => {
if (!report.name.trim()) {
alert('Please enter a report name');
return;
}
setIsSaving(true);
try {
const payload = {
name: report.name,
page_width: report.pageWidth,
page_height: report.pageHeight,
api_endpoint: report.apiEndpoint,
elements: report.elements
};
if (reportId) {
// Update existing report
await api.put(`/api/reports/${reportId}/`, payload);
} else {
// Create new report
const res = await api.post('/api/reports/', payload);
setReportId(res.data.id);
}
alert('Report saved successfully');
} catch (err) {
console.error('Failed to save report:', err);
alert('Failed to save report: ' + (err.response?.data?.detail || err.message));
} finally {
setIsSaving(false);
}
};
// Load reports list for dialog
const handleLoadClick = async () => {
setLoadingReports(true);
setShowLoadDialog(true);
try {
const res = await api.get('/api/reports/');
setSavedReports(res.data.results || res.data || []);
} catch (err) {
console.error('Failed to load reports list:', err);
alert('Failed to load reports list');
} finally {
setLoadingReports(false);
}
};
// Load a specific report
const handleLoadReport = async (id) => {
try {
const res = await api.get(`/api/reports/${id}/`);
const data = res.data;
setReport({
name: data.name,
pageWidth: data.page_width,
pageHeight: data.page_height,
apiEndpoint: data.api_endpoint || '',
elements: data.elements || []
});
setReportId(data.id);
setSelectedElementIds([]);
setShowLoadDialog(false);
} catch (err) {
console.error('Failed to load report:', err);
alert('Failed to load report');
}
};
// Delete a report
const handleDeleteReport = async (id) => {
if (!window.confirm('Are you sure you want to delete this report?')) return;
try {
await api.delete(`/api/reports/${id}/`);
setSavedReports(prev => prev.filter(r => r.id !== id));
if (reportId === id) {
setReportId(null);
}
} catch (err) {
console.error('Failed to delete report:', err);
alert('Failed to delete report');
}
};
// New report
const handleNewReport = () => {
if (report.elements.length > 0) {
if (!window.confirm('Discard current report and create a new one?')) return;
}
setReport({
name: 'Untitled Report',
pageWidth: 80,
pageHeight: 66,
apiEndpoint: '',
elements: []
});
setReportId(null);
setSelectedElementIds([]);
setShowLoadDialog(false);
};
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
@@ -217,6 +331,11 @@ function ReportEditorContent() {
previewMode={previewMode}
onTogglePreview={handleTogglePreview}
onConfigureAPI={() => setShowConfigPanel(true)}
onSave={handleSave}
onLoad={handleLoadClick}
reportName={report.name}
onReportNameChange={handleReportNameChange}
isSaving={isSaving}
/>
<div className="editor-layout">
<EditorCanvas
@@ -257,6 +376,53 @@ function ReportEditorContent() {
}}
/>
)}
{/* Load Report Dialog */}
{showLoadDialog && (
<div className="load-dialog-overlay" onClick={() => setShowLoadDialog(false)}>
<div className="load-dialog" onClick={e => e.stopPropagation()}>
<div className="load-dialog-header">
<h2>Load Report</h2>
<button className="load-dialog-close" onClick={() => setShowLoadDialog(false)}>×</button>
</div>
<div className="load-dialog-actions">
<button className="load-dialog-new-btn" onClick={handleNewReport}>
+ New Report
</button>
</div>
<div className="load-dialog-content">
{loadingReports ? (
<div className="load-dialog-loading">Loading...</div>
) : savedReports.length === 0 ? (
<div className="load-dialog-empty">No saved reports</div>
) : (
<ul className="load-dialog-list">
{savedReports.map(r => (
<li
key={r.id}
className={`load-dialog-item ${reportId === r.id ? 'load-dialog-item--current' : ''}`}
>
<div className="load-dialog-item-info" onClick={() => handleLoadReport(r.id)}>
<span className="load-dialog-item-name">{r.name}</span>
<span className="load-dialog-item-date">
{new Date(r.updated_at).toLocaleDateString()}
</span>
</div>
<button
className="load-dialog-item-delete"
onClick={() => handleDeleteReport(r.id)}
title="Delete report"
>
🗑
</button>
</li>
))}
</ul>
)}
</div>
</div>
</div>
)}
</div>
);
}
@@ -50,3 +50,33 @@
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.toolbar-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.toolbar-group--file {
margin-left: auto;
}
.toolbar-report-name {
padding: 8px 12px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
font-size: 14px;
width: 200px;
color: #333;
}
.toolbar-report-name::placeholder {
color: #999;
}
.toolbar-report-name:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
}
@@ -8,7 +8,12 @@ function Toolbar({
onBorderStyleChange,
previewMode = false,
onTogglePreview,
onConfigureAPI
onConfigureAPI,
onSave,
onLoad,
reportName,
onReportNameChange,
isSaving = false
}) {
return (
<div className="toolbar">
@@ -107,6 +112,32 @@ function Toolbar({
</button>
)}
</div>
<div className="toolbar-separator"></div>
<div className="toolbar-group toolbar-group--file">
<input
type="text"
className="toolbar-report-name"
value={reportName || ''}
onChange={e => onReportNameChange && onReportNameChange(e.target.value)}
placeholder="Report name"
title="Report name"
/>
<button
className="toolbar-button"
onClick={onSave}
disabled={isSaving}
title="Save Report"
>
{isSaving ? '...' : '💾 Save'}
</button>
<button
className="toolbar-button"
onClick={onLoad}
title="Load Report"
>
📂 Load
</button>
</div>
</div>
);
}