Web: Chargen auswaehlen und Lagerort der Auswahl setzen

Chargenliste bekommt Auswahl-Checkboxen. Das obere Panel wirkt jetzt auf die
Auswahl: "N ausgewaehlt -> Lagerort setzen" (leer = Ort entfernen, z.B. beim
Aufloesen eines Orts). Schnellwahl "alle sichtbaren" (respektiert Filter) und
"alle ohne Lagerort". DataTable meldet dafuer die sichtbaren Zeilen
(onVisibleRows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-29 14:04:19 +02:00
parent 826f4df4bd
commit 51c02091ed
2 changed files with 59 additions and 11 deletions

View File

@@ -29,6 +29,7 @@ function savePersisted(id, data) {
export default function DataTable({ export default function DataTable({
id, columns, rows, getRowKey, rowClassName, id, columns, rows, getRowKey, rowClassName,
empty = "Nichts gefunden.", loading = false, toolbarExtra = null, tree = null, empty = "Nichts gefunden.", loading = false, toolbarExtra = null, tree = null,
onVisibleRows = null,
}) { }) {
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]); const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
@@ -218,6 +219,15 @@ export default function DataTable({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [treeActive, treeData, flat, collapsed]); }, [treeActive, treeData, flat, collapsed]);
// Sichtbare (gefilterte/sortierte) Zeilen nach außen melden z.B. damit eine
// Seite „alle sichtbaren auswählen" anbieten kann. Ref, damit eine inline
// übergebene Funktion keine Endlosschleife auslöst.
const onVisibleRef = useRef(onVisibleRows);
onVisibleRef.current = onVisibleRows;
useEffect(() => {
onVisibleRef.current?.(displayRows.map((d) => d.row));
}, [displayRows]);
function toggleSort(c) { function toggleSort(c) {
if (!c.sortValue && !c.filterText) return; if (!c.sortValue && !c.filterText) return;
setSort((s) => { setSort((s) => {

View File

@@ -32,6 +32,8 @@ export default function Charges() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [bulkLoc, setBulkLoc] = useState(""); const [bulkLoc, setBulkLoc] = useState("");
const [selected, setSelected] = useState(() => new Set());
const [visible, setVisible] = useState([]); // aktuell gefiltert sichtbare Chargen
async function load() { async function load() {
setLoading(true); setLoading(true);
@@ -49,6 +51,17 @@ export default function Charges() {
const ohneOrt = useMemo(() => lots.filter((l) => !l.location_id), [lots]); const ohneOrt = useMemo(() => lots.filter((l) => !l.location_id), [lots]);
function toggle(id) {
setSelected((alt) => {
const neu = new Set(alt);
if (neu.has(id)) neu.delete(id); else neu.add(id);
return neu;
});
}
// Menge von IDs zur Auswahl hinzufügen (für „alle sichtbaren/ohne Ort").
function selectAll(ids) { setSelected(new Set(ids)); }
function clearSelection() { setSelected(new Set()); }
async function setLocation(lotId, locId) { async function setLocation(lotId, locId) {
setError(null); setError(null);
try { try {
@@ -59,12 +72,14 @@ export default function Charges() {
} }
} }
async function applyBulk() { // Lagerort für die aktuell ausgewählten Chargen setzen (leer = Ort entfernen).
if (!bulkLoc || ohneOrt.length === 0) return; async function applyToSelection() {
if (selected.size === 0) return;
setError(null); setError(null);
try { try {
await api.bulkLotLocation(ohneOrt.map((l) => l.id), bulkLoc); await api.bulkLotLocation([...selected], bulkLoc);
setBulkLoc(""); setBulkLoc("");
clearSelection();
await load(); await load();
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@@ -72,6 +87,12 @@ export default function Charges() {
} }
const columns = [ const columns = [
{ key: "select", header: "", label: "Auswahl", fixed: true, width: 44,
render: (l) => (
<input type="checkbox" style={{ width: "auto", margin: 0, cursor: "pointer" }}
checked={selected.has(l.id)} onChange={() => toggle(l.id)}
aria-label={`${l.product_name} auswählen`} />
) },
{ key: "thumb", header: "", label: "Bild", fixed: true, width: 56, { key: "thumb", header: "", label: "Bild", fixed: true, width: 56,
render: (l) => <ProduktThumb productId={l.product_id} alt={l.product_name} /> }, render: (l) => <ProduktThumb productId={l.product_id} alt={l.product_name} /> },
{ key: "product", header: "Artikel", grow: true, min: 180, { key: "product", header: "Artikel", grow: true, min: 180,
@@ -120,26 +141,43 @@ export default function Charges() {
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>} {error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
{isAdmin && ohneOrt.length > 0 && ( {isAdmin && (
<div className="card" style={{ marginBottom: "var(--sp-3)" }}> <div className="card" style={{ marginBottom: "var(--sp-3)" }}>
<div className="card-head"><Icon name="location" /><h2>Lagerort für alle ohne Ort setzen</h2></div> <div className="card-head"><Icon name="location" /><h2>Lagerort für ausgewählte Chargen setzen</h2></div>
<p className="muted small mt-0"> <p className="muted small mt-0">
{ohneOrt.length} Charge(n) haben noch keinen Lagerort. Ort wählen und in einem Klick allen zuweisen. {selected.size > 0
? `${selected.size} Charge(n) ausgewählt diese bekommen den gewählten Lagerort (leer = Ort entfernen).`
: "Chargen in der Tabelle anhaken, dann hier den Lagerort setzen. Oder unten schnell alle sichtbaren / alle ohne Ort auswählen."}
</p> </p>
<div className="field-inline"> <div className="field-inline" style={{ flexWrap: "wrap" }}>
<select value={bulkLoc} onChange={(e) => setBulkLoc(e.target.value)} style={{ maxWidth: 360 }}> <select value={bulkLoc} onChange={(e) => setBulkLoc(e.target.value)} style={{ maxWidth: 360 }}
<option value=""> Lagerort wählen </option> disabled={selected.size === 0}>
<option value=""> ohne Lagerort </option>
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)} {locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select> </select>
<button className="btn primary" disabled={!bulkLoc} onClick={applyBulk}> <button className="btn primary" disabled={selected.size === 0} onClick={applyToSelection}>
<Icon name="check" size={16} />Auf alle {ohneOrt.length} anwenden <Icon name="check" size={16} />Auf {selected.size} Auswahl anwenden
</button> </button>
</div> </div>
<div className="field-inline" style={{ flexWrap: "wrap", marginTop: "var(--sp-2)", marginBottom: 0 }}>
<button type="button" className="btn sm ghost" onClick={() => selectAll(visible.map((l) => l.id))}
disabled={visible.length === 0}>
Alle {visible.length} sichtbaren auswählen
</button>
<button type="button" className="btn sm ghost" onClick={() => selectAll(ohneOrt.map((l) => l.id))}
disabled={ohneOrt.length === 0}>
Alle {ohneOrt.length} ohne Lagerort auswählen
</button>
{selected.size > 0 && (
<button type="button" className="btn sm ghost" onClick={clearSelection}>Auswahl aufheben</button>
)}
</div>
</div> </div>
)} )}
<div className="card"> <div className="card">
<DataTable id="charges" columns={columns} rows={lots} loading={loading} <DataTable id="charges" columns={columns} rows={lots} loading={loading}
onVisibleRows={setVisible}
getRowKey={(l) => l.id} empty="Keine Chargen im Bestand." /> getRowKey={(l) => l.id} empty="Keine Chargen im Bestand." />
</div> </div>
</div> </div>