Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang

Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.

iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 12:15:25 +02:00
parent 6e48cd0a7a
commit d518b4aa23
23 changed files with 507 additions and 220 deletions

View File

@@ -181,7 +181,7 @@ struct TreeMasterView<Item: TreeItem, Header: View, Editor: View>: View {
let title: String
let singular: String
let load: () async throws -> [Item]
let delete: (Int) async throws -> Void
let delete: (Item.ID) async throws -> Void
/// Optionaler kleiner Hinweis am Zeilenende (z. B. der Kategorie-Typ).
let badge: (Item) -> String?
/// Nur Einträge, die das erfüllen, werden gezeigt (z. B. der Typ-Filter).
@@ -190,7 +190,7 @@ struct TreeMasterView<Item: TreeItem, Header: View, Editor: View>: View {
@ViewBuilder let editor: (_ editing: Item?, _ all: [Item], _ done: @escaping () -> Void) -> Editor
@State private var items: [Item] = []
@State private var collapsed: Set<Int> = []
@State private var collapsed: Set<Item.ID> = []
@State private var busy = true
@State private var error: String?
@State private var editing: Item?
@@ -200,7 +200,7 @@ struct TreeMasterView<Item: TreeItem, Header: View, Editor: View>: View {
private struct Node: Identifiable {
let item: Item
let depth: Int
var id: Int { item.id }
var id: Item.ID { item.id }
}
private var shown: [Item] { items.filter(include) }
@@ -221,11 +221,11 @@ struct TreeMasterView<Item: TreeItem, Header: View, Editor: View>: View {
return result
}
private func childCount(_ id: Int) -> Int { shown.filter { $0.parentId == id }.count }
private func childCount(_ id: Item.ID) -> Int { shown.filter { $0.parentId == id }.count }
private var visible: [Node] {
let parent = Dictionary(uniqueKeysWithValues: shown.map { ($0.id, $0.parentId) })
func hidden(_ id: Int) -> Bool {
func hidden(_ id: Item.ID) -> Bool {
var p = parent[id] ?? nil
while let cur = p {
if collapsed.contains(cur) { return true }
@@ -324,7 +324,7 @@ struct TreeMasterView<Item: TreeItem, Header: View, Editor: View>: View {
.padding(.leading, CGFloat(node.depth) * 16)
}
private func toggle(_ id: Int) {
private func toggle(_ id: Item.ID) {
if collapsed.contains(id) { collapsed.remove(id) } else { collapsed.insert(id) }
}
@@ -441,7 +441,7 @@ struct LocationEditor: View {
let done: () -> Void
@State private var name: String
@State private var parentId: Int?
@State private var parentId: String?
@State private var error: String?
@State private var busy = false
@@ -467,9 +467,9 @@ struct LocationEditor: View {
}
Section {
Picker("Übergeordnet", selection: $parentId) {
Text(" oberste Ebene ").tag(Int?.none)
Text(" oberste Ebene ").tag(String?.none)
ForEach(parentOptions) { loc in
Text(LocationEditor.pfad(loc, in: all)).tag(Int?.some(loc.id))
Text(LocationEditor.pfad(loc, in: all)).tag(String?.some(loc.id))
}
}
} header: {
@@ -513,8 +513,8 @@ struct LocationEditor: View {
}
/// Alle Unterorte (rekursiv) als Ziel beim Umhängen ausgeschlossen.
private static func descendants(of id: Int, in all: [StorageLocation]) -> Set<Int> {
var result: Set<Int> = []
private static func descendants(of id: String, in all: [StorageLocation]) -> Set<String> {
var result: Set<String> = []
var stack = [id]
while let cur = stack.popLast() {
for kid in all where kid.parentId == cur {