diff --git a/web/src/components/CategorySelect.jsx b/web/src/components/CategorySelect.jsx index 00c6892..bc40ea4 100644 --- a/web/src/components/CategorySelect.jsx +++ b/web/src/components/CategorySelect.jsx @@ -20,7 +20,11 @@ export default function CategorySelect({ // Zusätzliche Einträge über dem Baum, z.B. { value: 0, label: "Ohne Kategorie" } // für Filter. Ihr Wert wird unverändert an onChange gereicht. extraOptions = [], + // Aus, wo „nichts gewaehlt" gar kein gueltiger Wert ist (Mindestbestaende: + // dort heisst die oberste Zeile „Ueberall" und steht in extraOptions). + showRoot = true, disabled = false, + title, }) { const [open, setOpen] = useState(false); const [collapsed, setCollapsed] = useState(() => new Set()); @@ -123,6 +127,7 @@ export default function CategorySelect({ ref={btnRef} className="tree-select-btn" disabled={disabled} + title={title} aria-expanded={open} onClick={() => setOpen((o) => !o)} > @@ -138,14 +143,16 @@ export default function CategorySelect({ className="tree-pop" style={{ left: pos.left, top: pos.top, minWidth: Math.max(pos.width, 220) }} > - pick(null)} - > - {rootLabel} - + {showRoot && ( + pick(null)} + > + {rootLabel} + + )} {extraOptions.map((o) => ( {it.uid} {isAdmin ? ( - patchItem(it, { location_id: e.target.value === "" ? null : e.target.value })}> - – ohne – - {locationOptions(locations).map((o) => {o.label})} - + patchItem(it, { location_id: id === "" ? null : id })} /> ) : {ortName(it.location_id) || "–"}} @@ -488,11 +486,8 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh onChange={(e) => setAddForm({ ...addForm, count: e.target.value })} /> Lagerort - setAddForm({ ...addForm, location_id: e.target.value })}> - – ohne – - {locationOptions(locations).map((o) => {o.label})} - + setAddForm({ ...addForm, location_id: id })} /> Gekauft bei braucht es einen Wert, deshalb hier ein @@ -70,9 +71,9 @@ export default function LocationMinStock({ locations, initial = [], unitLabel = {rows.map((r, i) => ( - setRow(i, { location_id: e.target.value })}> - {alleOrte.map((o) => {o.label})} - + setRow(i, { location_id: id })} /> , in dem + * jede Zeile den ganzen Pfad ausschrieb; Geschwister standen dadurch nur + * zufällig beieinander. + * + * `CategorySelect` kann beides – seine IDs dürfen Zeichenketten sein, und + * Lagerort-Codes sind welche. Hier kommt nur der Baumaufbau dazu und die + * Umrechnung "" ↔ null, damit die Aufrufstellen weiter mit dem leeren String + * für „kein Ort" arbeiten wie beim zuvor. + */ +export default function LocationSelect({ + value, + onChange, + locations, + rootLabel = "– ohne –", + extraOptions = [], + showRoot = true, + disabled = false, + title, +}) { + return ( + onChange(id == null ? "" : id)} + // Nach Namen sortieren, bevor der Baum entsteht: Der Server sortiert + // zeichenweise, dort stuende „Fach oben Rechts" hinter „Fach Unten + // Rechts". asTree uebernimmt die Reihenfolge der Geschwister unveraendert. + nodes={asTree([...(locations || [])].sort((a, b) => a.name.localeCompare(b.name, "de")))} + rootLabel={rootLabel} + extraOptions={extraOptions} + showRoot={showRoot} + disabled={disabled} + title={title} + /> + ); +} diff --git a/web/src/components/ObjektBestand.jsx b/web/src/components/ObjektBestand.jsx index 4132ad0..7e5d644 100644 --- a/web/src/components/ObjektBestand.jsx +++ b/web/src/components/ObjektBestand.jsx @@ -4,7 +4,8 @@ import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import Icon from "./Icon"; import { fmt } from "../units"; -import { locationOptions, locationPathById } from "../locationPath"; +import { locationPathById } from "../locationPath"; +import LocationSelect from "./LocationSelect"; // Gründe zum Entfernen (Wert = Backend-Enum, Label = Anzeige). export const REASONS = [ @@ -182,11 +183,9 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha Lagerort - setAddForm({ ...addForm, location_id: e.target.value })}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + setAddForm({ ...addForm, location_id: id })} /> Menge @@ -208,16 +207,14 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha Von - setMove({ ...move, from: e.target.value })}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + setMove({ ...move, from: id })} /> Nach - setMove({ ...move, to: e.target.value })}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + setMove({ ...move, to: id })} /> Menge Lagerort - setRemove({ ...remove, location_id: e.target.value })}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + setRemove({ ...remove, location_id: id })} /> Menge nach Lagerort - setLocId(e.target.value)}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + {rest != null && !zielGleich && ( diff --git a/web/src/dashboard/KartenConfig.jsx b/web/src/dashboard/KartenConfig.jsx index 2f58099..f1730d2 100644 --- a/web/src/dashboard/KartenConfig.jsx +++ b/web/src/dashboard/KartenConfig.jsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import Icon from "../components/Icon"; import { ZEITRAEUME } from "./cards"; -import { locationOptions } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { artikelLabel } from "../units"; /** @@ -28,11 +28,9 @@ export default function KartenConfig({ return ( {f.label}{f.hint ? ({f.hint}) : null} - onProps({ [f.key]: e.target.value || undefined })}> - – alle Orte – - {locationOptions(locations).map((o) => {o.label})} - + onProps({ [f.key]: id || undefined })} /> ); } diff --git a/web/src/pages/Charges.jsx b/web/src/pages/Charges.jsx index eeedfcd..73a7c6a 100644 --- a/web/src/pages/Charges.jsx +++ b/web/src/pages/Charges.jsx @@ -10,7 +10,8 @@ import { ProduktThumb } from "../components/ProduktBild"; import { useSettings } from "../settings"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; -import { locationOptions, locationPathById } from "../locationPath"; +import { locationPathById } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { expiryRowClass, faelligAm, fmt, gebinde, unitShort } from "../units"; // Artikeleinheit einer Charge (Gebinde, sonst Produkteinheit). @@ -243,11 +244,8 @@ export default function Charges() { filterText: (l) => (l.location_id ? (locationPathById(l.location_id, locations) || "") : "(ohne)"), filterValues: (l) => [l.location_id ? (locationPathById(l.location_id, locations) || "?") : "(ohne)"], render: (l) => (isAdmin ? ( - setLocation(l.id, e.target.value)}> - – ohne – - {locationOptions(locations).map((o) => {o.label})} - + setLocation(l.id, id)} /> ) : ( l.location_id ? (locationPathById(l.location_id, locations) || "?") : – ohne – )) }, @@ -305,11 +303,9 @@ export default function Charges() { : "Chargen in der Tabelle anhaken, dann hier handeln. Oder unten schnell alle sichtbaren / alle ohne Ort auswählen."} - setBulkLoc(e.target.value)} style={{ maxWidth: 360 }} - disabled={selected.size === 0}> - – ohne Lagerort – - {locationOptions(locations).map((o) => {o.label})} - + Lagerort setzen diff --git a/web/src/pages/CheckIn.jsx b/web/src/pages/CheckIn.jsx index 329fb57..7f9a3e8 100644 --- a/web/src/pages/CheckIn.jsx +++ b/web/src/pages/CheckIn.jsx @@ -9,7 +9,7 @@ import { useToast } from "../toast"; import { suggestionToProduct } from "../offUtils"; import { asTree } from "../categoryTree"; import CategorySelect from "../components/CategorySelect"; -import { locationOptions } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units"; import { gruppenOptionen } from "../groupGraph"; @@ -405,10 +405,8 @@ export default function CheckIn() { Lagerort (optional) - setLocationId(e.target.value)}> - – keiner – - {locationOptions(locations).map((o) => {o.label})} - + diff --git a/web/src/pages/ItemList.jsx b/web/src/pages/ItemList.jsx index 7f150b0..b3b1adc 100644 --- a/web/src/pages/ItemList.jsx +++ b/web/src/pages/ItemList.jsx @@ -5,7 +5,8 @@ import { useAuth } from "../auth"; import Icon from "../components/Icon"; import DataTable from "../components/DataTable"; import { ProduktThumb } from "../components/ProduktBild"; -import { locationOptions, locationPathById } from "../locationPath"; +import { locationPathById } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { categoryInfoMap } from "../categoryPath"; import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel"; import { @@ -73,11 +74,8 @@ export default function ItemList() { { key: "location", header: "Lagerort", width: 220, filterText: (it) => locationPathById(it.location_id, locations), render: (it) => (isAdmin ? ( - patch(it, { location_id: e.target.value === "" ? null : e.target.value })}> - – ohne – - {locationOptions(locations).map((o) => {o.label})} - + patch(it, { location_id: id === "" ? null : id })} /> ) : (locationPathById(it.location_id, locations) || "–")) }, { key: "loan", header: "Verliehen an", width: 200, filterText: (it) => it.loan?.borrower || "", diff --git a/web/src/pages/MinStock.jsx b/web/src/pages/MinStock.jsx index 6b58d8d..ee8ea47 100644 --- a/web/src/pages/MinStock.jsx +++ b/web/src/pages/MinStock.jsx @@ -4,6 +4,7 @@ import { useAuth } from "../auth"; import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; import { locationOptions, locationPathById } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { gruppenOptionen } from "../groupGraph"; import { anzahlWort, fmt, gebinde, grpFactor, grpPkgMode, grpUnit, kindShort } from "../units"; @@ -308,12 +309,10 @@ export default function MinStock() { {isAdmin && ( <> - moveRow(r, e.target.value)}> - Überall - {locationOptions(locations).map((o) => {o.label})} - + showRoot={false} extraOptions={[{ value: UEBERALL, label: "Überall" }]} + onChange={(id) => moveRow(r, id)} /> deleteRow(r)}> > @@ -350,10 +349,9 @@ export default function MinStock() { Wo - setDraft({ ...draft, locId: e.target.value })}> - Überall (egal wo) - {locationOptions(locations).map((o) => {o.label})} - + setDraft({ ...draft, locId: id })} /> Was diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index 7a93334..46a09c1 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -16,7 +16,8 @@ import { DynamicFields, FIELD_TYPES } from "../fields"; import ObjektBestand from "../components/ObjektBestand"; import Einzelstuecke from "../components/Einzelstuecke"; import SplitLotDialog from "../components/SplitLotDialog"; -import { locationOptions, locationPathById } from "../locationPath"; +import { locationPathById } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { gruppenOptionen } from "../groupGraph"; import { BASE_UNITS, daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired, @@ -1354,13 +1355,8 @@ function LotsCard({ product, lots, locations = [], baseShort, warnDays, isAdmin, {editing ? ( - setDraft({ ...draft, location_id: e.target.value })}> - – ohne – - {locationOptions(locations).map((o) => ( - {o.label} - ))} - + setDraft({ ...draft, location_id: id })} /> ) : ( l.location_id ? (locationPathById(l.location_id, locations) || "?")
diff --git a/web/src/dashboard/KartenConfig.jsx b/web/src/dashboard/KartenConfig.jsx index 2f58099..f1730d2 100644 --- a/web/src/dashboard/KartenConfig.jsx +++ b/web/src/dashboard/KartenConfig.jsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import Icon from "../components/Icon"; import { ZEITRAEUME } from "./cards"; -import { locationOptions } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { artikelLabel } from "../units"; /** @@ -28,11 +28,9 @@ export default function KartenConfig({ return ( {f.label}{f.hint ? ({f.hint}) : null} - onProps({ [f.key]: e.target.value || undefined })}> - – alle Orte – - {locationOptions(locations).map((o) => {o.label})} - + onProps({ [f.key]: id || undefined })} /> ); } diff --git a/web/src/pages/Charges.jsx b/web/src/pages/Charges.jsx index eeedfcd..73a7c6a 100644 --- a/web/src/pages/Charges.jsx +++ b/web/src/pages/Charges.jsx @@ -10,7 +10,8 @@ import { ProduktThumb } from "../components/ProduktBild"; import { useSettings } from "../settings"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; -import { locationOptions, locationPathById } from "../locationPath"; +import { locationPathById } from "../locationPath"; +import LocationSelect from "../components/LocationSelect"; import { expiryRowClass, faelligAm, fmt, gebinde, unitShort } from "../units"; // Artikeleinheit einer Charge (Gebinde, sonst Produkteinheit). @@ -243,11 +244,8 @@ export default function Charges() { filterText: (l) => (l.location_id ? (locationPathById(l.location_id, locations) || "") : "(ohne)"), filterValues: (l) => [l.location_id ? (locationPathById(l.location_id, locations) || "?") : "(ohne)"], render: (l) => (isAdmin ? ( - setLocation(l.id, e.target.value)}> - – ohne – - {locationOptions(locations).map((o) => {o.label})} - + setLocation(l.id, id)} /> ) : ( l.location_id ? (locationPathById(l.location_id, locations) || "?") : – ohne – )) }, @@ -305,11 +303,9 @@ export default function Charges() { : "Chargen in der Tabelle anhaken, dann hier handeln. Oder unten schnell alle sichtbaren / alle ohne Ort auswählen."}