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