added vehicles and vehicles extra, dynamic tables for entities, dynamic dropdowns with overlay for selecting and manage data
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+166
@@ -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);
|
||||
}
|
||||
|
||||
+32
-1
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user