Add quick-fill table for vehicle extra data
Shows previously-used combinations of lookup nomenclature values above the Additional Data fields in both the new-vehicle and edit forms. Clicking a row merges the combination into the current values, so free-text fields the operator already typed are preserved. NomenclatureContext now fans extra_combination events out to subscribers; without that they hit the unknown-object warning and were dropped, so deletes would not have reached other open tabs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNomenclatures } from '../contexts/NomenclatureContext';
|
||||
import { useNomenclatureData } from '../contexts/NomenclatureDataContext';
|
||||
import api from '../services/api';
|
||||
import './ExtraCombinationsTable.css';
|
||||
|
||||
export default function ExtraCombinationsTable({ nomenclatures, onApply }) {
|
||||
const { subscribeToUpdates } = useNomenclatures();
|
||||
const { getDisplayValue } = useNomenclatureData();
|
||||
|
||||
const [combinations, setCombinations] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// subscribeToUpdates is not memoized in NomenclatureContext - hold it in a ref
|
||||
// so the SSE effect below runs once instead of on every provider render.
|
||||
const subscribeRef = useRef(subscribeToUpdates);
|
||||
subscribeRef.current = subscribeToUpdates;
|
||||
|
||||
// Only lookup nomenclatures form a combination
|
||||
const lookupNomenclatures = nomenclatures.filter(n => n.kind === 'lookup');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.get('/api/extra-combinations/');
|
||||
setCombinations(res.data.results || res.data);
|
||||
} catch {
|
||||
setError('Failed to load saved combinations');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// Realtime: rows are created/bumped by the backend when extra data is saved
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeRef.current((message) => {
|
||||
if (message.object !== 'extra_combination') return;
|
||||
const { operation, data } = message;
|
||||
|
||||
setCombinations(prev => {
|
||||
if (operation === 'delete') {
|
||||
return prev.filter(c => c.id !== data.id);
|
||||
}
|
||||
// insert or update: drop any existing copy, put it on top (most recent first)
|
||||
return [data, ...prev.filter(c => c.id !== data.id)];
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (e, id) => {
|
||||
e.stopPropagation();
|
||||
if (!window.confirm('Delete this combination?')) return;
|
||||
setError('');
|
||||
try {
|
||||
await api.delete(`/api/extra-combinations/${id}/`);
|
||||
} catch {
|
||||
setError('Failed to delete combination');
|
||||
}
|
||||
};
|
||||
|
||||
if (lookupNomenclatures.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="extra-combos">
|
||||
<h3>Previously Used</h3>
|
||||
{error && <div className="extra-combos-error">{error}</div>}
|
||||
<div className="extra-combos-table-container">
|
||||
<table className="extra-combos-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{lookupNomenclatures.map(nom => (
|
||||
<th key={nom.code}>{nom.name}</th>
|
||||
))}
|
||||
<th className="extra-combos-th-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{combinations.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={lookupNomenclatures.length + 1} className="extra-combos-empty">
|
||||
No saved combinations yet
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
combinations.map(combo => (
|
||||
<tr
|
||||
key={combo.id}
|
||||
className="extra-combos-row"
|
||||
onClick={() => onApply(combo.data)}
|
||||
title="Click to fill the fields below"
|
||||
>
|
||||
{lookupNomenclatures.map(nom => {
|
||||
const entryId = combo.data[nom.code];
|
||||
const label = entryId != null ? getDisplayValue(nom.code, entryId) : '';
|
||||
return (
|
||||
<td key={nom.code}>
|
||||
{entryId != null && !label
|
||||
? <span className="extra-combos-stale">#{entryId}</span>
|
||||
: label}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="extra-combos-td-actions">
|
||||
<button
|
||||
className="extra-combos-delete-btn"
|
||||
onClick={(e) => handleDelete(e, combo.id)}
|
||||
title="Delete combination"
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user