Gruppen hierarchisch: in allen Auswahlfeldern und in der Artikelliste

Die Gruppen-Dropdowns waren flache, alphabetische Listen - man sah nicht, dass
"Grillwurst" unter "Wurst" haengt. Ein gemeinsamer Helfer (gruppenOptionen)
rollt den Graphen zum Baum aus und rueckt ein; eine Gruppe mit mehreren
Obergruppen erscheint unter jeder, denn genau das ist ihr Sinn, und ausgewaehlt
wird ohnehin dieselbe ID.

Eingerueckt wird mit GESCHUETZTEN Leerzeichen - normale fasst der Browser in
einem <option> zusammen, die Einrueckung waere sonst wirkungslos.

Betroffen: Gruppen-Auswahl im Artikelformular (beide Zweige), beim Einlagern,
die Ziel-Auswahl der Mindestbestaende und die Obergruppen-Mehrfachauswahl (die
beim Suchen bewusst abflacht, weil eine Einrueckung ohne die Eltern in die Irre
fuehrt). In der App dasselbe in beiden Gruppen-Pickern.

Die Gruppen-Spalte der Artikelliste zeigt jetzt den Weg von oben statt eines
Etiketts - "Wurst → Grillwurst", mit gedaempften Vorfahren wie bei der
Kategorie. Gefiltert wird ueber ALLE Wege: wer auf "Wurst" filtert, sieht auch
die Artikel aus den Untergruppen. Dafuer gibt es gruppenInfoMap als Gegenstueck
zu categoryInfoMap, das mit mehreren Wegen je Gruppe umgehen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-16 09:59:32 +02:00
parent 302e7625e5
commit e00eef275c
9 changed files with 184 additions and 36 deletions

View File

@@ -327,3 +327,47 @@ struct GroupParentPicker: View {
.navigationBarTitleDisplayMode(.inline)
}
}
/// Eine Zeile fuer ein Gruppen-Auswahlfeld: Gruppe plus ihre Tiefe im Baum.
struct GruppenOption: Identifiable {
let id: String // Pfad eine Gruppe kann unter mehreren haengen
let gruppe: GroupItem
let tiefe: Int
/// Eingerueckte Beschriftung fuer den Picker.
var label: String {
String(repeating: " ", count: tiefe) + (tiefe > 0 ? "" : "") + gruppe.name
}
}
/// Gruppen als Baum ausrollen fuer Auswahlfelder.
///
/// Eine Gruppe mit mehreren Obergruppen erscheint unter JEDER; das ist der Sinn
/// mehrerer Obergruppen, und ausgewaehlt wird ohnehin dieselbe ID.
func gruppenOptionen(_ groups: [GroupItem]) -> [GruppenOption] {
let vorhanden = Set(groups.map(\.id))
var kinderVon: [Int: [GroupItem]] = [:]
var wurzeln: [GroupItem] = []
for g in groups {
let eltern = (g.parentIds ?? []).filter { vorhanden.contains($0) }
if eltern.isEmpty { wurzeln.append(g) }
for e in eltern { kinderVon[e, default: []].append(g) }
}
let nachName: (GroupItem, GroupItem) -> Bool = {
$0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
}
var out: [GruppenOption] = []
func walk(_ g: GroupItem, _ tiefe: Int, _ pfad: String, _ gesehen: Set<Int>) {
let eigen = "\(pfad)/\(g.id)"
out.append(GruppenOption(id: eigen, gruppe: g, tiefe: tiefe))
if gesehen.contains(g.id) { return } // Ringschutz
var weiter = gesehen
weiter.insert(g.id)
for k in (kinderVon[g.id] ?? []).sorted(by: nachName) {
walk(k, tiefe + 1, eigen, weiter)
}
}
for w in wurzeln.sorted(by: nachName) { walk(w, 0, "", []) }
return out
}

View File

@@ -217,8 +217,9 @@ struct ProductDetailView: View {
Section {
Picker("Gruppe", selection: $groupId) {
Text(" keine ").tag(Int?.none)
ForEach(groups) { gruppe in
Text(gruppe.name).tag(Int?.some(gruppe.id))
// Als Baum, damit sichtbar ist, was unter was haengt.
ForEach(gruppenOptionen(groups)) { eintrag in
Text(eintrag.label).tag(Int?.some(eintrag.gruppe.id))
}
}
} footer: {

View File

@@ -166,8 +166,9 @@ struct ProductFormView: View {
Section {
Picker("Gruppe", selection: $selectedGroupId) {
Text(" keine ").tag(Int?.none)
ForEach(groups) { gruppe in
Text(gruppe.name).tag(Int?.some(gruppe.id))
// Als Baum, damit sichtbar ist, was unter was haengt.
ForEach(gruppenOptionen(groups)) { eintrag in
Text(eintrag.label).tag(Int?.some(eintrag.gruppe.id))
}
}
if session.isAdmin {