Mengenfeld beschriftet und "Angemeldet bleiben" ergaenzt

Beim Einlagern stand in der Chargenzeile eine nackte 1 ohne Beschriftung. Das
las sich wie eine Nummerierung der Charge, war aber das Mengenfeld. Die Zeile
hat jetzt links die Beschriftung "Menge", der Wert steht rechtsbuendig; die 1
bleibt als Vorgabe, weil meistens genau ein Stueck eingelagert wird. Beim
Auslagern war dieselbe Zeile ebenso unbeschriftet und ist jetzt gleich aufgebaut.

Anmeldung: Das Token wanderte bisher immer dauerhaft in den Schluesselbund.
Der neue Haken "Angemeldet bleiben" laesst sich abwaehlen - dann gilt die
Anmeldung nur, solange die App laeuft, und es bleibt nichts auf dem Geraet
zurueck. Die Wahl wird gemerkt, damit der Haken beim naechsten Mal richtig steht.

Dabei ist aufgefallen, dass der Benutzername nirgends gespeichert wurde: Nach
einem Neustart stand im Menue nur noch "Angemeldet als" ohne Namen. Er wird
jetzt zusammen mit dem Token abgelegt und beim Abmelden wieder entfernt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 17:35:10 +02:00
parent 8e94f15c84
commit d680aeff0e
5 changed files with 53 additions and 4 deletions

View File

@@ -6,6 +6,14 @@
{ {
"layers" : [ "layers" : [
{ {
"fill-specializations" : [
{
"appearance" : "dark",
"value" : {
"solid" : "extended-gray:1.00000,1.00000"
}
}
],
"glass" : false, "glass" : false,
"image-name" : "barcode-gelesen.svg", "image-name" : "barcode-gelesen.svg",
"name" : "barcode-gelesen", "name" : "barcode-gelesen",

View File

@@ -67,8 +67,14 @@ struct CheckInFormView: View {
ForEach($lines) { $line in ForEach($lines) { $line in
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
HStack { HStack {
// Ohne Beschriftung sah die vorbelegte 1 wie eine
// Nummerierung der Charge aus.
Text("Menge")
Spacer(minLength: 12)
TextField("Menge", text: $line.quantity) TextField("Menge", text: $line.quantity)
.keyboardType(.decimalPad) .keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: 110)
if lines.count > 1 { if lines.count > 1 {
Button(role: .destructive) { Button(role: .destructive) {
lines.removeAll { $0.id == line.id } lines.removeAll { $0.id == line.id }

View File

@@ -155,7 +155,14 @@ struct CheckOutFormView: View {
} }
Section("Menge") { Section("Menge") {
TextField("Menge", text: $quantity).keyboardType(.decimalPad) HStack {
Text("Menge")
Spacer(minLength: 12)
TextField("Menge", text: $quantity)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(maxWidth: 110)
}
Picker("Einheit", selection: $unit) { Picker("Einheit", selection: $unit) {
ForEach(unitOptions, id: \.self) { Text($0).tag($0) } ForEach(unitOptions, id: \.self) { Text($0).tag($0) }
} }

View File

@@ -8,6 +8,7 @@ struct LoginView: View {
@State private var password = "" @State private var password = ""
@State private var error: String? @State private var error: String?
@State private var busy = false @State private var busy = false
@State private var stayLoggedIn = Session.shared.stayLoggedIn
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -23,6 +24,13 @@ struct LoginView: View {
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.autocorrectionDisabled() .autocorrectionDisabled()
SecureField("Passwort", text: $password) SecureField("Passwort", text: $password)
Toggle("Angemeldet bleiben", isOn: $stayLoggedIn)
}
if !stayLoggedIn {
Section {
Text("Die Anmeldung gilt nur, solange die App geoeffnet ist.")
.font(.caption).foregroundStyle(.secondary)
}
} }
if let error { if let error {
Section { Text(error).foregroundStyle(.red).font(.callout) } Section { Text(error).foregroundStyle(.red).font(.callout) }
@@ -43,7 +51,8 @@ struct LoginView: View {
session.setServer(server) session.setServer(server)
do { do {
let token = try await APIClient.shared.login(username: username, password: password) let token = try await APIClient.shared.login(username: username, password: password)
session.store(token: token.accessToken, username: token.username, isAdmin: token.role == "admin") session.store(token: token.accessToken, username: token.username,
isAdmin: token.role == "admin", persist: stayLoggedIn)
} catch { } catch {
self.error = error.localizedDescription self.error = error.localizedDescription
} }

View File

@@ -7,12 +7,16 @@ final class Session: ObservableObject {
static let shared = Session() static let shared = Session()
private let urlKey = "server_url" private let urlKey = "server_url"
private let usernameKey = "username"
private let stayKey = "stay_logged_in"
private let keychainAccount = "project-good-token" private let keychainAccount = "project-good-token"
@Published private(set) var baseURL: URL? @Published private(set) var baseURL: URL?
@Published private(set) var token: String? @Published private(set) var token: String?
@Published var username: String = "" @Published var username: String = ""
@Published var isAdmin: Bool = false @Published var isAdmin: Bool = false
/// Merkt die letzte Wahl, damit der Haken im Login richtig steht.
@Published private(set) var stayLoggedIn: Bool = true
var isLoggedIn: Bool { token != nil && baseURL != nil } var isLoggedIn: Bool { token != nil && baseURL != nil }
@@ -20,7 +24,10 @@ final class Session: ObservableObject {
if let stored = UserDefaults.standard.string(forKey: urlKey) { if let stored = UserDefaults.standard.string(forKey: urlKey) {
baseURL = Session.normalize(stored) baseURL = Session.normalize(stored)
} }
// Ohne bisherige Wahl bleibt man angemeldet - das ist der Alltagsfall.
stayLoggedIn = UserDefaults.standard.object(forKey: stayKey) as? Bool ?? true
token = Keychain.read(account: keychainAccount) token = Keychain.read(account: keychainAccount)
username = UserDefaults.standard.string(forKey: usernameKey) ?? ""
} }
/// Sorgt für eine URL mit Schema und abschließendem "/", damit relative /// Sorgt für eine URL mit Schema und abschließendem "/", damit relative
@@ -39,11 +46,22 @@ final class Session: ObservableObject {
UserDefaults.standard.set(url.absoluteString, forKey: urlKey) UserDefaults.standard.set(url.absoluteString, forKey: urlKey)
} }
func store(token newToken: String, username name: String, isAdmin admin: Bool) { /// `persist == false` heisst: die Anmeldung gilt nur, solange die App laeuft.
/// Auf dem Geraet bleibt dann nichts zurueck.
func store(token newToken: String, username name: String, isAdmin admin: Bool, persist: Bool) {
token = newToken token = newToken
username = name username = name
isAdmin = admin isAdmin = admin
stayLoggedIn = persist
UserDefaults.standard.set(persist, forKey: stayKey)
if persist {
Keychain.write(newToken, account: keychainAccount) Keychain.write(newToken, account: keychainAccount)
UserDefaults.standard.set(name, forKey: usernameKey)
} else {
Keychain.delete(account: keychainAccount)
UserDefaults.standard.removeObject(forKey: usernameKey)
}
} }
func logout() { func logout() {
@@ -51,6 +69,7 @@ final class Session: ObservableObject {
username = "" username = ""
isAdmin = false isAdmin = false
Keychain.delete(account: keychainAccount) Keychain.delete(account: keychainAccount)
UserDefaults.standard.removeObject(forKey: usernameKey)
} }
} }