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; // Lookups and selectors form a combination: the selector value is what makes // "Ship + cargo" and "Warehouse + cargo" distinct rows rather than one. const lookupNomenclatures = nomenclatures.filter( n => n.kind === 'lookup' || n.kind === 'selector' ); // A selector stores its option value, not an entry id, so it resolves to the // option's label instead of going through getDisplayValue. const cellLabel = (nom, value) => { if (nom.kind !== 'selector') return getDisplayValue(nom.code, value); const option = (nom.selector_options || []).find(o => o.value === value); return option ? option.label : ''; }; 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 (

Previously Used

{error &&
{error}
}
{lookupNomenclatures.map(nom => ( ))} {combinations.length === 0 ? ( ) : ( combinations.map(combo => ( onApply(combo.data)} title="Click to fill the fields below" > {lookupNomenclatures.map(nom => { const entryId = combo.data[nom.code]; const label = entryId != null ? cellLabel(nom, entryId) : ''; return ( ); })} )) )}
{nom.name}
No saved combinations yet
{entryId != null && !label ? #{entryId} : label}
); }