added docker-compose for linux
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Header from './Header';
|
||||
import useSerialData from '../hooks/useSerialData';
|
||||
import { useNomenclatures } from '../contexts/NomenclatureContext';
|
||||
import { useNomenclatureData } from '../contexts/NomenclatureDataContext';
|
||||
import NomenclatureDropdown from './NomenclatureUI/NomenclatureDropdown';
|
||||
import ReportPreviewModal from './ReportPreviewModal';
|
||||
import api from '../services/api';
|
||||
import './Main.css';
|
||||
|
||||
export default function Main() {
|
||||
const navigate = useNavigate();
|
||||
const { readings, isConnected } = useSerialData();
|
||||
const { vehicles } = useNomenclatures();
|
||||
|
||||
@@ -18,9 +21,10 @@ export default function Main() {
|
||||
const [newExtraData, setNewExtraData] = useState({});
|
||||
const [error, setError] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showNoticeReport, setShowNoticeReport] = useState(false);
|
||||
|
||||
// Nomenclature data from context
|
||||
const { definitions } = useNomenclatureData();
|
||||
const { definitions, entries } = useNomenclatureData();
|
||||
const vehicleNomenclatures = useMemo(
|
||||
() => Object.values(definitions)
|
||||
.filter(d => d.applies_to === 'vehicle')
|
||||
@@ -164,6 +168,31 @@ export default function Main() {
|
||||
? selectedVehicle.gross - selectedVehicle.tare
|
||||
: null;
|
||||
|
||||
// Vehicle data shaped for report preview/design
|
||||
// Transforms extra.data keys from raw codes/ids to { [nomenclature name]: display value }
|
||||
const vehicleReportData = useMemo(() => {
|
||||
if (!selectedVehicle) return null;
|
||||
const rawExtra = selectedVehicle.extra?.data || {};
|
||||
const extra = {};
|
||||
for (const [code, value] of Object.entries(rawExtra)) {
|
||||
const def = definitions[code];
|
||||
if (!def) continue;
|
||||
if (def.kind === 'lookup') {
|
||||
const entry = (entries[code] || []).find(e => e.id === value);
|
||||
extra[def.name] = entry?.display_value ?? '';
|
||||
} else {
|
||||
extra[def.name] = value ?? '';
|
||||
}
|
||||
}
|
||||
return {
|
||||
vehicle: {
|
||||
...selectedVehicle,
|
||||
net: netWeight,
|
||||
extra,
|
||||
}
|
||||
};
|
||||
}, [selectedVehicle, netWeight, definitions, entries]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
@@ -363,6 +392,23 @@ export default function Main() {
|
||||
<div className="weight-card weight-card--net">
|
||||
<div className="weight-card-header">Net</div>
|
||||
<div className="weight-card-value">{netWeight} kg</div>
|
||||
<button
|
||||
className="weight-set-btn"
|
||||
onClick={() => setShowNoticeReport(true)}
|
||||
>
|
||||
Print Notice
|
||||
</button>
|
||||
<button
|
||||
className="weight-set-btn"
|
||||
onClick={() => navigate('/report-editor', {
|
||||
state: {
|
||||
reportName: 'notice_report',
|
||||
vehicleData: vehicleReportData,
|
||||
}
|
||||
})}
|
||||
>
|
||||
Design Notice
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -398,6 +444,12 @@ export default function Main() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showNoticeReport && vehicleReportData && (
|
||||
<ReportPreviewModal
|
||||
vehicleData={vehicleReportData}
|
||||
onClose={() => setShowNoticeReport(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,30 @@ import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
function formatValue(raw, dataType) {
|
||||
if (raw === '' || raw === null || raw === undefined) return '';
|
||||
switch (dataType) {
|
||||
case 'number': {
|
||||
const n = Number(raw);
|
||||
return isNaN(n) ? '' : String(n);
|
||||
}
|
||||
case 'date': {
|
||||
const d = new Date(raw);
|
||||
return isNaN(d.getTime()) ? '' : d.toLocaleDateString();
|
||||
}
|
||||
case 'time': {
|
||||
const d = new Date(raw);
|
||||
return isNaN(d.getTime()) ? '' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
case 'datetime': {
|
||||
const d = new Date(raw);
|
||||
return isNaN(d.getTime()) ? '' : d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
default:
|
||||
return String(raw);
|
||||
}
|
||||
}
|
||||
|
||||
function DBTextField({
|
||||
element,
|
||||
isSelected,
|
||||
@@ -51,12 +75,16 @@ function DBTextField({
|
||||
};
|
||||
|
||||
// Resolve data value
|
||||
const displayContent = previewMode
|
||||
const rawValue = previewMode
|
||||
? resolveDBTextValue(reportData, element.objectKey, element.fieldPath, parentBandId, { currentBandData })
|
||||
: parentBandId
|
||||
? `{${element.fieldPath}}` // Inside band: show field only
|
||||
: `{${element.objectKey}.${element.fieldPath}}`; // Outside band: show object.field
|
||||
|
||||
const displayContent = previewMode
|
||||
? formatValue(rawValue, element.dataType || 'general')
|
||||
: rawValue;
|
||||
|
||||
// Check if data is resolved
|
||||
// If inside a band, only fieldPath is required; otherwise both objectKey and fieldPath are required
|
||||
const isUnresolved = parentBandId
|
||||
@@ -71,7 +99,8 @@ function DBTextField({
|
||||
...getElementStyle(element, charWidth, charHeight),
|
||||
minWidth: `${charWidth}px`,
|
||||
minHeight: `${charHeight}px`,
|
||||
whiteSpace
|
||||
whiteSpace,
|
||||
textAlign: element.alignment || 'left',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -237,6 +237,32 @@ function ObjectInspector({ element, onUpdate, allElements = [] }) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="property-row">
|
||||
<label>Data Type:</label>
|
||||
<select
|
||||
value={element.dataType || 'general'}
|
||||
onChange={(e) => handleChange('dataType', e.target.value)}
|
||||
>
|
||||
<option value="general">General</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="time">Time</option>
|
||||
<option value="datetime">Date & Time</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Alignment:</label>
|
||||
<select
|
||||
value={element.alignment || 'left'}
|
||||
onChange={(e) => handleChange('alignment', e.target.value)}
|
||||
>
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DataProvider } from './DataContext';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { DataProvider, useReportData } from './DataContext';
|
||||
import { BandProvider } from './BandContext';
|
||||
import Toolbar from './Toolbar';
|
||||
import EditorCanvas from './EditorCanvas';
|
||||
@@ -33,6 +34,38 @@ function ReportEditorContent() {
|
||||
const [savedReports, setSavedReports] = useState([]);
|
||||
const [loadingReports, setLoadingReports] = useState(false);
|
||||
|
||||
// Auto-load report and vehicle data passed via router state
|
||||
const location = useLocation();
|
||||
const { setData } = useReportData();
|
||||
|
||||
useEffect(() => {
|
||||
const { reportName, vehicleData } = location.state || {};
|
||||
if (!reportName) return;
|
||||
|
||||
api.get('/api/reports/').then(res => {
|
||||
const reports = res.data.results || res.data;
|
||||
const found = reports.find(r => r.name === reportName);
|
||||
if (found) {
|
||||
setReport({
|
||||
name: found.name,
|
||||
pageWidth: found.page_width,
|
||||
pageHeight: found.page_height,
|
||||
apiEndpoint: found.api_endpoint || '',
|
||||
elements: found.elements || [],
|
||||
});
|
||||
setReportId(found.id);
|
||||
} else {
|
||||
// Report doesn't exist yet — pre-fill the name so Save creates it
|
||||
setReport(prev => ({ ...prev, name: reportName }));
|
||||
}
|
||||
if (vehicleData) {
|
||||
setData(vehicleData);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to auto-load report:', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleAddElement = (element) => {
|
||||
setReport(prev => {
|
||||
// Check if the new element is positioned inside a band
|
||||
|
||||
@@ -113,12 +113,14 @@ export class Element {
|
||||
* Database-bound text field element
|
||||
*/
|
||||
export class DBTextElement extends Element {
|
||||
constructor({ id, x, y, width = 10, height = 1, objectKey = '', fieldPath = '' }) {
|
||||
constructor({ id, x, y, width = 10, height = 1, objectKey = '', fieldPath = '', dataType = 'general', alignment = 'left' }) {
|
||||
super({ id, type: 'dbtext', x, y });
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.objectKey = objectKey;
|
||||
this.fieldPath = fieldPath;
|
||||
this.dataType = dataType;
|
||||
this.alignment = alignment;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
@@ -148,7 +150,9 @@ export class DBTextElement extends Element {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
objectKey: this.objectKey,
|
||||
fieldPath: this.fieldPath
|
||||
fieldPath: this.fieldPath,
|
||||
dataType: this.dataType,
|
||||
alignment: this.alignment
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.report-preview-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.report-preview-modal {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.report-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.report-preview-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 0 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.report-preview-close:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.report-preview-body {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.report-preview-error {
|
||||
padding: 24px;
|
||||
color: #c00;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.report-preview-loading {
|
||||
padding: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DataProvider, useReportData } from './ReportEditor/DataContext';
|
||||
import { BandProvider } from './ReportEditor/BandContext';
|
||||
import EditorCanvas from './ReportEditor/EditorCanvas';
|
||||
import api from '../services/api';
|
||||
import './ReportPreviewModal.css';
|
||||
|
||||
function PreviewContent({ vehicleData, elements }) {
|
||||
const { setData } = useReportData();
|
||||
|
||||
useEffect(() => {
|
||||
setData(vehicleData);
|
||||
}, [vehicleData]);
|
||||
|
||||
return (
|
||||
<BandProvider>
|
||||
<EditorCanvas
|
||||
elements={elements}
|
||||
selectedElementIds={[]}
|
||||
previewMode={true}
|
||||
toolMode="select"
|
||||
borderStyle="single"
|
||||
onElementSelect={() => {}}
|
||||
onSelectMultiple={() => {}}
|
||||
onDeselectAll={() => {}}
|
||||
onElementUpdate={() => {}}
|
||||
onElementDelete={() => {}}
|
||||
onAddElement={() => {}}
|
||||
/>
|
||||
</BandProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReportPreviewModal({ vehicleData, onClose }) {
|
||||
const [elements, setElements] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/api/reports/')
|
||||
.then(res => {
|
||||
const reports = res.data.results || res.data;
|
||||
const report = reports.find(r => r.name === 'notice_report');
|
||||
if (report) {
|
||||
setElements(report.elements || []);
|
||||
} else {
|
||||
setError('Report "notice_report" not found. Create it in the Report Editor first.');
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Failed to load report'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="report-preview-overlay" onClick={onClose}>
|
||||
<div className="report-preview-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="report-preview-header">
|
||||
<span>Notice Report — {vehicleData.vehicle.vehicle_number}</span>
|
||||
<button className="report-preview-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
<div className="report-preview-body">
|
||||
{error ? (
|
||||
<div className="report-preview-error">{error}</div>
|
||||
) : elements === null ? (
|
||||
<div className="report-preview-loading">Loading...</div>
|
||||
) : (
|
||||
<DataProvider>
|
||||
<PreviewContent vehicleData={vehicleData} elements={elements} />
|
||||
</DataProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user