Gleiche Idee wie im Web: ein Typ-Umschalter filtert die Kategorieauswahl, statt Werkzeug & Co. zwischen den Lebensmittel-Kategorien zu suchen. Im Anlege-Formular blenden sich zudem die Lebensmittel-Felder (Gruppe, Einheit, MHD) aus, wenn "Gegenstand" gewaehlt ist. Die Detailansicht filtert die Kategorieliste auf den Typ des Artikels. CategoryPicker bekam dafuer einen optionalen Typ-Filter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.8 KiB
Swift
114 lines
3.8 KiB
Swift
import SwiftUI
|
||
|
||
/// Auswahl einer Kategorie mit eingerueckten Unterkategorien.
|
||
///
|
||
/// Kategorie und Gruppe sind zwei verschiedene Dinge: Die Kategorie ordnet nur
|
||
/// die Artikelliste, die Gruppe zaehlt Bestaende mehrerer Marken zusammen.
|
||
struct CategoryPicker: View {
|
||
let categories: [CategoryItem]
|
||
@Binding var selection: Int?
|
||
/// Nur Kategorien dieses Typs zeigen ("food"/"object"); nil = alle.
|
||
var tracking: String? = nil
|
||
|
||
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))
|
||
}
|
||
}
|
||
}
|
||
|
||
private var gefiltert: [CategoryItem] {
|
||
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
|
||
/// verlassen muss. Nur fuer Administratoren - POST /groups verlangt das.
|
||
struct NewGroupSheet: View {
|
||
let units: [Unit]
|
||
var onCreated: (GroupItem) -> Void
|
||
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@State private var name = ""
|
||
@State private var minStock = ""
|
||
@State private var unitId: Int?
|
||
@State private var busy = false
|
||
@State private var error: String?
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section {
|
||
LabeledField(label: "Name", text: $name)
|
||
} footer: {
|
||
Text("Eine Gruppe zählt Bestände mehrerer Marken zusammen – "
|
||
+ "z. B. Mehl von verschiedenen Herstellern als „5 kg Mehl“.")
|
||
}
|
||
|
||
Section("Mindestbestand (optional)") {
|
||
QuantityField(label: "Menge", text: $minStock)
|
||
Picker("Einheit", selection: $unitId) {
|
||
Text("– Basiseinheit –").tag(Int?.none)
|
||
ForEach(units) { unit in
|
||
Text(unit.name).tag(Int?.some(unit.id))
|
||
}
|
||
}
|
||
}
|
||
|
||
if let error {
|
||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||
}
|
||
|
||
Section {
|
||
Button(busy ? "Anlegen…" : "Anlegen") { Task { await create() } }
|
||
.disabled(busy || name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
}
|
||
}
|
||
.navigationTitle("Neue Gruppe")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||
}
|
||
}
|
||
|
||
private func create() async {
|
||
error = nil
|
||
busy = true
|
||
defer { busy = false }
|
||
do {
|
||
let created = try await APIClient.shared.createGroup(
|
||
NewGroupRequest(
|
||
name: name.trimmingCharacters(in: .whitespaces),
|
||
minStock: Double(minStock.replacingOccurrences(of: ",", with: ".")),
|
||
minStockUnitId: unitId
|
||
)
|
||
)
|
||
onCreated(created)
|
||
dismiss()
|
||
} catch {
|
||
self.error = error.localizedDescription
|
||
}
|
||
}
|
||
}
|