diff --git a/ios/Sources/Baum.swift b/ios/Sources/Baum.swift new file mode 100644 index 0000000..c0335d9 --- /dev/null +++ b/ios/Sources/Baum.swift @@ -0,0 +1,38 @@ +import Foundation + +/// Eine Zeile einer flach ausgelegten Baumliste, mit ihrer Tiefe. +/// +/// Lagerorte und Kategorien sind Baeume, kommen vom Server aber als flache +/// Liste nach Namen sortiert. Daraus wird eine Auswahl, in der "Garage → Fach A" +/// und "Keller → Fach A" irgendwo zwischen allem anderen stehen - Geschwister +/// und Kinder finden nie zusammen. Diese Reihenfolge stellt das her. +struct Baumzeile: Identifiable { + let item: T + let tiefe: Int + + var id: T.ID { item.id } + + /// Name mit Einrueckung je Ebene. Macht die Hierarchie in einer flachen + /// Auswahl sichtbar, ohne jede Zeile mit dem vollen Pfad zu ueberladen. + var label: String { String(repeating: " ", count: tiefe) + item.name } +} + +/// Bringt eine flache Eltern-Kind-Liste in Baumreihenfolge, mit Tiefe. +/// +/// Waisen - deren Elternteil weggefiltert oder geloescht ist - gelten als +/// oberste Ebene. Ohne diese Regel verschwaenden sie lautlos aus der Auswahl. +func baum(_ liste: [T]) -> [Baumzeile] { + let ids = Set(liste.map(\.id)) + var ergebnis: [Baumzeile] = [] + + func gehe(_ knoten: T, _ tiefe: Int) { + ergebnis.append(Baumzeile(item: knoten, tiefe: tiefe)) + for kind in liste.filter({ $0.parentId == knoten.id }) { + gehe(kind, tiefe + 1) + } + } + for wurzel in liste.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { + gehe(wurzel, 0) + } + return ergebnis +} diff --git a/ios/Sources/CategoryPicker.swift b/ios/Sources/CategoryPicker.swift index 219f31b..5472077 100644 --- a/ios/Sources/CategoryPicker.swift +++ b/ios/Sources/CategoryPicker.swift @@ -13,9 +13,8 @@ struct CategoryPicker: View { var body: some View { Picker("Kategorie", selection: $selection) { Text("– keine –").tag(Int?.none) - ForEach(flattened(), id: \.item.id) { eintrag in - Text(String(repeating: " ", count: eintrag.depth) + eintrag.item.name) - .tag(Int?.some(eintrag.item.id)) + ForEach(baum(gefiltert)) { eintrag in + Text(eintrag.label).tag(Int?.some(eintrag.item.id)) } } } @@ -24,23 +23,6 @@ struct CategoryPicker: View { guard let tracking else { return categories } return categories.filter { $0.tracking == tracking } } - - private func flattened() -> [(item: CategoryItem, depth: Int)] { - let quelle = gefiltert - let ids = Set(quelle.map(\.id)) - var result: [(CategoryItem, Int)] = [] - - func walk(_ node: CategoryItem, _ depth: Int) { - result.append((node, depth)) - for child in quelle.filter({ $0.parentId == node.id }) { - walk(child, depth + 1) - } - } - for root in quelle.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { - walk(root, 0) - } - return result - } } /// Legt eine Gruppe an Ort und Stelle an, damit man das Formular nicht diff --git a/ios/Sources/CheckInFormView.swift b/ios/Sources/CheckInFormView.swift index d84420c..e71150c 100644 --- a/ios/Sources/CheckInFormView.swift +++ b/ios/Sources/CheckInFormView.swift @@ -211,8 +211,8 @@ struct CheckInFormView: View { Section { Picker("Lagerort", selection: $locationId) { Text("– keiner –").tag(String?.none) - ForEach(locations) { location in - Text(location.path(in: locations)).tag(String?.some(location.id)) + ForEach(baum(locations)) { eintrag in + Text(eintrag.label).tag(String?.some(eintrag.item.id)) } } Button { diff --git a/ios/Sources/CheckInView.swift b/ios/Sources/CheckInView.swift index 55b0277..d065c62 100644 --- a/ios/Sources/CheckInView.swift +++ b/ios/Sources/CheckInView.swift @@ -675,7 +675,11 @@ struct KassenzettelPruefenView: View { HStack { Menu { Button("– ohne –") { z.wrappedValue.ortId = nil } - ForEach(locations) { loc in Button(loc.name) { z.wrappedValue.ortId = loc.id } } + // Voller Pfad statt nur des Namens: In dieser engen Zeile gibt es + // keine Einrueckung, und "Fach A" allein ist nicht eindeutig. + ForEach(locations) { loc in + Button(loc.path(in: locations)) { z.wrappedValue.ortId = loc.id } + } } label: { Label(ortName(zeile.ortId), systemImage: "mappin.and.ellipse") } diff --git a/ios/Sources/GroupViews.swift b/ios/Sources/GroupViews.swift index fda3674..89fd495 100644 --- a/ios/Sources/GroupViews.swift +++ b/ios/Sources/GroupViews.swift @@ -217,8 +217,8 @@ struct GroupEditor: View { HStack { Picker("Ort", selection: row.locationId) { Text(UEBERALL_NAME).tag(String?.some(UEBERALL_ID)) - ForEach(locations) { l in - Text(l.path(in: locations)).tag(String?.some(l.id)) + ForEach(baum(locations)) { eintrag in + Text(eintrag.label).tag(String?.some(eintrag.item.id)) } } TextField("Menge", text: row.amount) diff --git a/ios/Sources/ItemViews.swift b/ios/Sources/ItemViews.swift index b18a835..6bd585f 100644 --- a/ios/Sources/ItemViews.swift +++ b/ios/Sources/ItemViews.swift @@ -340,7 +340,7 @@ struct ItemEditView: View { Section { Picker("Lagerort", selection: $locationId) { Text("– ohne –").tag(String?.none) - ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } + ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) } } // Lagerort per QR setzen – ohne den (evtl. nicht vorhandenen) QR // des Einzelstücks. Praktisch z.B. bei Kleidung. @@ -831,7 +831,7 @@ struct ItemAddSheet: View { Stepper("Anzahl: \(count)", value: $count, in: 1...200) Picker("Lagerort", selection: $locationId) { Text("– ohne –").tag(String?.none) - ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } + ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) } } Button { locScanShown = true } label: { Label("Lagerort scannen", systemImage: "qrcode.viewfinder") diff --git a/ios/Sources/MasterDataViews.swift b/ios/Sources/MasterDataViews.swift index 4a737c1..0d40890 100644 --- a/ios/Sources/MasterDataViews.swift +++ b/ios/Sources/MasterDataViews.swift @@ -197,33 +197,11 @@ struct TreeMasterView: View { @State private var addShown = false @State private var pendingDeletion: Item? - private struct Node: Identifiable { - let item: Item - let depth: Int - var id: Item.ID { item.id } - } - private var shown: [Item] { items.filter(include) } - /// Baumreihenfolge mit Tiefe. Waisen (Elternteil gefiltert/gelöscht) gelten - /// als oberste Ebene, damit nichts verschwindet. - private var nodes: [Node] { - let list = shown - let ids = Set(list.map(\.id)) - var result: [Node] = [] - func walk(_ n: Item, _ depth: Int) { - result.append(Node(item: n, depth: depth)) - for child in list.filter({ $0.parentId == n.id }) { walk(child, depth + 1) } - } - for root in list.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { - walk(root, 0) - } - return result - } - private func childCount(_ id: Item.ID) -> Int { shown.filter { $0.parentId == id }.count } - private var visible: [Node] { + private var visible: [Baumzeile] { let parent = Dictionary(uniqueKeysWithValues: shown.map { ($0.id, $0.parentId) }) func hidden(_ id: Item.ID) -> Bool { var p = parent[id] ?? nil @@ -233,7 +211,7 @@ struct TreeMasterView: View { } return false } - return nodes.filter { !hidden($0.item.id) } + return baum(shown).filter { !hidden($0.item.id) } } var body: some View { @@ -286,7 +264,7 @@ struct TreeMasterView: View { } @ViewBuilder - private func row(_ node: Node) -> some View { + private func row(_ node: Baumzeile) -> some View { let kinder = childCount(node.item.id) let zu = collapsed.contains(node.item.id) HStack(spacing: 8) { @@ -321,7 +299,7 @@ struct TreeMasterView: View { } .buttonStyle(.plain) } - .padding(.leading, CGFloat(node.depth) * 16) + .padding(.leading, CGFloat(node.tiefe) * 16) } private func toggle(_ id: Item.ID) { @@ -468,8 +446,8 @@ struct LocationEditor: View { Section { Picker("Übergeordnet", selection: $parentId) { Text("– oberste Ebene –").tag(String?.none) - ForEach(parentOptions) { loc in - Text(LocationEditor.pfad(loc, in: all)).tag(String?.some(loc.id)) + ForEach(baum(parentOptions)) { eintrag in + Text(eintrag.label).tag(String?.some(eintrag.item.id)) } } } header: { @@ -523,19 +501,6 @@ struct LocationEditor: View { } return result } - - /// "Keller → Regal" – der Pfad macht gleiche Namen im flachen Picker - /// unterscheidbar. - private static func pfad(_ loc: StorageLocation, in all: [StorageLocation]) -> String { - let byId = Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) }) - var teile = [loc.name] - var pid = loc.parentId - while let cur = pid, let parent = byId[cur] { - teile.insert(parent.name, at: 0) - pid = parent.parentId - } - return teile.joined(separator: " → ") - } } struct UnitsView: View { @@ -820,8 +785,8 @@ struct CategoryEditor: View { Section { Picker("Übergeordnet", selection: $parentId) { Text("– oberste Ebene –").tag(Int?.none) - ForEach(all) { c in - Text(CategoryEditor.pfad(c, in: all)).tag(Int?.some(c.id)) + ForEach(baum(all)) { eintrag in + Text(eintrag.label).tag(Int?.some(eintrag.item.id)) } } } header: { @@ -896,17 +861,6 @@ struct CategoryEditor: View { self.error = error.localizedDescription } } - - private static func pfad(_ c: CategoryItem, in all: [CategoryItem]) -> String { - let byId = Dictionary(uniqueKeysWithValues: all.map { ($0.id, $0) }) - var teile = [c.name] - var pid = c.parentId - while let cur = pid, let parent = byId[cur] { - teile.insert(parent.name, at: 0) - pid = parent.parentId - } - return teile.joined(separator: " → ") - } } // MARK: - Eigene Felder je Kategorie diff --git a/ios/Sources/ObjectStockView.swift b/ios/Sources/ObjectStockView.swift index 0270f1e..5510eae 100644 --- a/ios/Sources/ObjectStockView.swift +++ b/ios/Sources/ObjectStockView.swift @@ -152,7 +152,7 @@ private struct LocationPicker: View { var body: some View { Picker(title, selection: $selection) { Text("– ohne Lagerort –").tag(String?.none) - ForEach(locations) { loc in Text(loc.path(in: locations)).tag(String?.some(loc.id)) } + ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) } } } } diff --git a/ios/Sources/ProductDetailView.swift b/ios/Sources/ProductDetailView.swift index 2763a72..08847ff 100644 --- a/ios/Sources/ProductDetailView.swift +++ b/ios/Sources/ProductDetailView.swift @@ -688,7 +688,7 @@ struct LotEditView: View { Section("Lagerort") { Picker("Lagerort", selection: $locationId) { Text("– ohne –").tag(String?.none) - ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } + ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) } } // Ort per QR am Regal/Fach setzen, statt in der Liste zu suchen. Button { locScanShown = true } label: { @@ -800,7 +800,7 @@ struct ProductLocationMinView: View { HStack { Picker("Ort", selection: $row.locationId) { Text(UEBERALL_NAME).tag(String?.some(UEBERALL_ID)) - ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } + ForEach(baum(locations)) { e in Text(e.label).tag(String?.some(e.item.id)) } } TextField("Menge", text: $row.amount) .keyboardType(.decimalPad) diff --git a/ios/Vorrania.xcodeproj/project.pbxproj b/ios/Vorrania.xcodeproj/project.pbxproj index b50cb99..e3faa17 100644 --- a/ios/Vorrania.xcodeproj/project.pbxproj +++ b/ios/Vorrania.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 83F9AD06177F3BC95421F128 /* DashboardCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090485CC54558EB4433376A9 /* DashboardCards.swift */; }; 87A34A269AAD85A19C3F4359 /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA706060734632DE78FA1073 /* ServerListView.swift */; }; 926451E72FD13708C5DD657B /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E6139CCD0B6C51B3919EE9 /* DashboardView.swift */; }; + 93BE44F5C09AA9A9B67ED211 /* Baum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F4CDB145DD84793DFC90F1 /* Baum.swift */; }; 96E5A2455B2EC26050E69232 /* MasterDataViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E86178BB91DBB86ADFB284B /* MasterDataViews.swift */; }; 9B0A1A7C19764857BDC6ED26 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4741D0E95875919C921945CF /* RootView.swift */; }; A7017E6C0532A650439AED7B /* BestBeforeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */; }; @@ -77,6 +78,7 @@ 76393ADA26304E5BC5AD7E7F /* AssignScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignScanView.swift; sourceTree = ""; }; 7B5BB49FBEFF88768B0BC902 /* ObjectStockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectStockView.swift; sourceTree = ""; }; 7DACFBB69CB536BA7CBBD484 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + 85F4CDB145DD84793DFC90F1 /* Baum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Baum.swift; sourceTree = ""; }; 8AF19A735AC17451B57BF5BB /* ScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannerView.swift; sourceTree = ""; }; 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = ""; }; 9948219CDDC4188EA4298F22 /* VorraniaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VorraniaApp.swift; sourceTree = ""; }; @@ -115,6 +117,7 @@ 378F1B2DE567B62169E84426 /* APIClient.swift */, 660ACEFA22BC77D52FBE736A /* AppDashboards.swift */, 76393ADA26304E5BC5AD7E7F /* AssignScanView.swift */, + 85F4CDB145DD84793DFC90F1 /* Baum.swift */, AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */, CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */, E580BA2815BB8F6341D64B47 /* ChartCards.swift */, @@ -253,6 +256,7 @@ 39F1F17DCB4E491652D08DEE /* APIClient.swift in Sources */, F3F718BFC60BF945CFA32BD2 /* AppDashboards.swift in Sources */, 5A0C3254B45CA13B1C3D9265 /* AssignScanView.swift in Sources */, + 93BE44F5C09AA9A9B67ED211 /* Baum.swift in Sources */, A7017E6C0532A650439AED7B /* BestBeforeText.swift in Sources */, 25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */, FFD4DB783F89083C9098F1C4 /* ChartCards.swift in Sources */, @@ -367,7 +371,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -390,7 +394,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffle.vorrania; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; diff --git a/ios/project.yml b/ios/project.yml index 3c2cc9d..fd6e9eb 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -52,7 +52,7 @@ targets: # MARKETING_VERSION ist die sichtbare Version (App Store), # CURRENT_PROJECT_VERSION die Build-Nummer: muss bei jedem Upload # innerhalb derselben Version steigen. - MARKETING_VERSION: "1.1" + MARKETING_VERSION: "1.2" CURRENT_PROJECT_VERSION: "2" TARGETED_DEVICE_FAMILY: "1,2" SWIFT_VERSION: "5.9"