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
+308
View File
@@ -0,0 +1,308 @@
import { useState, useEffect, useMemo } from 'react';
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 api from '../services/api';
import './Main.css';
export default function Main() {
const { readings, isConnected } = useSerialData();
const { vehicles } = useNomenclatures();
const [selectedVehicleId, setSelectedVehicleId] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [newVehicleNumber, setNewVehicleNumber] = useState('');
const [showNewForm, setShowNewForm] = useState(false);
const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false);
// Nomenclature data from context
const { definitions } = useNomenclatureData();
const vehicleNomenclatures = useMemo(
() => Object.values(definitions)
.filter(d => d.applies_to === 'vehicle')
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)),
[definitions]
);
const [extraData, setExtraData] = useState({});
// Current reading from COM port
// Data format: "Value: 36106.04" or just "36106.04"
const currentReading = readings.length > 0 ? readings[0].data : null;
const currentWeight = useMemo(() => {
if (!currentReading) return null;
// Extract numeric value - handle "Value: 123.45" or just "123.45"
const match = currentReading.match(/[\d.]+/);
if (match) {
const val = parseFloat(match[0]);
return isNaN(val) ? null : Math.round(val);
}
return null;
}, [currentReading]);
// Selected vehicle object (refreshes when vehicles list updates via SSE)
const selectedVehicle = useMemo(
() => vehicles.find(v => v.id === selectedVehicleId) || null,
[vehicles, selectedVehicleId]
);
// Filtered vehicles
const filteredVehicles = useMemo(() => {
if (!searchQuery.trim()) return vehicles;
const q = searchQuery.toLowerCase();
return vehicles.filter(v => v.vehicle_number.toLowerCase().includes(q));
}, [vehicles, searchQuery]);
// When vehicle selection changes, load its extra data
useEffect(() => {
if (selectedVehicle?.extra?.data) {
setExtraData({ ...selectedVehicle.extra.data });
} else {
setExtraData({});
}
}, [selectedVehicle]);
// Create new vehicle
const handleCreateVehicle = async () => {
if (!newVehicleNumber.trim()) return;
setIsSaving(true);
setError('');
try {
const res = await api.post('/api/vehicles/', { vehicle_number: newVehicleNumber.trim() });
setSelectedVehicleId(res.data.id);
setNewVehicleNumber('');
setShowNewForm(false);
} catch (err) {
const msg = err.response?.data?.vehicle_number?.[0] || err.response?.data?.detail || 'Failed to create vehicle';
setError(msg);
} finally {
setIsSaving(false);
}
};
// Set tare
const handleSetTare = async () => {
if (!selectedVehicle || currentWeight === null || isNaN(currentWeight)) return;
setError('');
try {
await api.post(`/api/vehicles/${selectedVehicle.id}/set-tare/`, { value: currentWeight });
} catch {
setError('Failed to set tare');
}
};
// Set gross
const handleSetGross = async () => {
if (!selectedVehicle || currentWeight === null || isNaN(currentWeight)) return;
setError('');
try {
await api.post(`/api/vehicles/${selectedVehicle.id}/set-gross/`, { value: currentWeight });
} catch {
setError('Failed to set gross');
}
};
// Save extra data
const handleSaveExtra = async () => {
if (!selectedVehicle) return;
setIsSaving(true);
setError('');
try {
await api.patch(`/api/vehicles/${selectedVehicle.id}/`, {
extra: { data: extraData },
});
} catch (err) {
const data = err.response?.data;
setError(typeof data === 'string' ? data : JSON.stringify(data) || 'Failed to save');
} finally {
setIsSaving(false);
}
};
// Format date
const formatDate = (dateStr) => {
if (!dateStr) return '';
const d = new Date(dateStr);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Net weight
const netWeight = (selectedVehicle?.tare != null && selectedVehicle?.gross != null)
? selectedVehicle.gross - selectedVehicle.tare
: null;
return (
<div>
<Header />
<div className="main">
{/* Left panel - Vehicle list */}
<div className="main-left">
<div className="vehicle-list-header">
<button
className="vehicle-add-btn"
onClick={() => { setShowNewForm(true); setError(''); }}
>
+ New Vehicle
</button>
<input
className="vehicle-search"
type="text"
placeholder="Search..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
{showNewForm && (
<div className="vehicle-new-form">
<input
type="text"
placeholder="Vehicle number"
value={newVehicleNumber}
onChange={e => setNewVehicleNumber(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleCreateVehicle()}
disabled={isSaving}
autoFocus
/>
<div className="vehicle-new-actions">
<button onClick={handleCreateVehicle} disabled={isSaving}>
{isSaving ? '...' : 'Create'}
</button>
<button onClick={() => { setShowNewForm(false); setNewVehicleNumber(''); setError(''); }}>
Cancel
</button>
</div>
</div>
)}
<div className="vehicle-list">
{filteredVehicles.map(v => (
<div
key={v.id}
className={`vehicle-list-item ${selectedVehicleId === v.id ? 'vehicle-list-item--active' : ''}`}
onClick={() => { setSelectedVehicleId(v.id); setError(''); }}
>
<span className="vehicle-list-number">{v.vehicle_number}</span>
{v.tare != null && (
<span className="vehicle-list-weight">{v.tare} kg</span>
)}
</div>
))}
{filteredVehicles.length === 0 && (
<div className="vehicle-list-empty">No vehicles found</div>
)}
</div>
</div>
{/* Right panel - Vehicle detail */}
<div className="main-right">
{!selectedVehicle ? (
<div className="main-placeholder">
Select a vehicle or create a new one
</div>
) : (
<div className="vehicle-detail">
{error && <div className="vehicle-error">{error}</div>}
<h2 className="vehicle-detail-title">{selectedVehicle.vehicle_number}</h2>
{/* Current reading */}
<div className="reading-display">
<label>Current Reading</label>
<div className={`reading-value ${isConnected ? '' : 'reading-value--disconnected'}`}>
{currentWeight !== null && !isNaN(currentWeight)
? `${currentWeight} kg`
: (isConnected ? 'Waiting...' : 'Disconnected')
}
</div>
</div>
{/* Weighing section */}
<div className="weighing-section">
{/* Tare */}
<div className="weight-card">
<div className="weight-card-header">Tare</div>
<div className="weight-card-value">
{selectedVehicle.tare != null ? `${selectedVehicle.tare} kg` : '---'}
</div>
{selectedVehicle.tare_date && (
<div className="weight-card-meta">
{formatDate(selectedVehicle.tare_date)}
{selectedVehicle.tare_user_name && ` (${selectedVehicle.tare_user_name})`}
</div>
)}
<button
className="weight-set-btn"
onClick={handleSetTare}
disabled={currentWeight === null || isNaN(currentWeight)}
>
Set Tare
</button>
</div>
{/* Gross */}
<div className="weight-card">
<div className="weight-card-header">Gross</div>
<div className="weight-card-value">
{selectedVehicle.gross != null ? `${selectedVehicle.gross} kg` : '---'}
</div>
{selectedVehicle.gross_date && (
<div className="weight-card-meta">
{formatDate(selectedVehicle.gross_date)}
{selectedVehicle.gross_user_name && ` (${selectedVehicle.gross_user_name})`}
</div>
)}
<button
className="weight-set-btn"
onClick={handleSetGross}
disabled={currentWeight === null || isNaN(currentWeight)}
>
Set Gross
</button>
</div>
{/* Net */}
{netWeight !== null && (
<div className="weight-card weight-card--net">
<div className="weight-card-header">Net</div>
<div className="weight-card-value">{netWeight} kg</div>
</div>
)}
</div>
{/* Extra fields from nomenclatures */}
{vehicleNomenclatures.length > 0 && (
<div className="extra-section">
<h3>Additional Data</h3>
<div className="extra-fields">
{vehicleNomenclatures.map(nom => (
<div key={nom.code} className="extra-field">
<NomenclatureDropdown
nomenclatureCode={nom.code}
value={extraData[nom.code]}
onChange={val => setExtraData(prev => ({
...prev,
[nom.code]: val,
}))}
/>
</div>
))}
</div>
<button
className="extra-save-btn"
onClick={handleSaveExtra}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Extra Data'}
</button>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}