iOS-App angefangen; Import-Modi; MHD-Formatierung; Gebinde in Ablauf-Ansichten
iOS (neu, ios/):
- SwiftUI-App: Login (Server + Keychain-Token), Kamera-Scanner (EAN/UPC/Code128),
Einlagern mit mehreren Chargen und eigenem MHD, Auslagern mit Chargenauswahl
oder FEFO, Artikelsuche, Anlegen mit Open-Food-Facts-Vorbefuellung.
- Home-Screen-Shortcuts: Schnellaktionen (langer Druck) und URL-Schema
projectgood://checkin | ://checkout fuer eigene Symbole via Kurzbefehle.
- project.yml (XcodeGen) + README mit Build-Anleitung. NICHT kompiliert - auf
diesem Rechner ist kein Xcode vorhanden.
Import-Modi (wie besprochen sinnvoll):
- add (Standard, nichts loeschen), replace_listed (Bestaende der in der Datei
genannten Produkte ersetzen - fuer Inventur), replace_all (alles ersetzen).
Geleerte Bestaende werden als Korrektur-Bewegung protokolliert; die Oberflaeche
fragt bei den zerstoerenden Modi nach.
Anzeige:
- "Bald ablaufend"/"Abgelaufen" zeigen jetzt das Gebinde ("1 Glas") mit der
Basiseinheit klein darunter (ExpiringItem liefert die Einheiten mit).
- MHD als Zeitspanne mit korrektem Numerus: "in 3 Tagen", "in 1 Woche",
"vor 2 Wochen", dazu heute/morgen/gestern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
95
ios/Sources/Session.swift
Normal file
95
ios/Sources/Session.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/// Hält Server-Adresse und Anmeldedaten. Das Token liegt im Keychain,
|
||||
/// die Server-URL in den UserDefaults (nicht geheim).
|
||||
final class Session: ObservableObject {
|
||||
static let shared = Session()
|
||||
|
||||
private let urlKey = "server_url"
|
||||
private let keychainAccount = "project-good-token"
|
||||
|
||||
@Published private(set) var baseURL: URL?
|
||||
@Published private(set) var token: String?
|
||||
@Published var username: String = ""
|
||||
@Published var isAdmin: Bool = false
|
||||
|
||||
var isLoggedIn: Bool { token != nil && baseURL != nil }
|
||||
|
||||
private init() {
|
||||
if let stored = UserDefaults.standard.string(forKey: urlKey) {
|
||||
baseURL = Session.normalize(stored)
|
||||
}
|
||||
token = Keychain.read(account: keychainAccount)
|
||||
}
|
||||
|
||||
/// Sorgt für eine URL mit Schema und abschließendem "/", damit relative
|
||||
/// Pfade ("api/...") korrekt aufgelöst werden.
|
||||
static func normalize(_ raw: String) -> URL? {
|
||||
var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else { return nil }
|
||||
if !text.contains("://") { text = "http://" + text }
|
||||
if !text.hasSuffix("/") { text += "/" }
|
||||
return URL(string: text)
|
||||
}
|
||||
|
||||
func setServer(_ raw: String) {
|
||||
guard let url = Session.normalize(raw) else { return }
|
||||
baseURL = url
|
||||
UserDefaults.standard.set(url.absoluteString, forKey: urlKey)
|
||||
}
|
||||
|
||||
func store(token newToken: String, username name: String, isAdmin admin: Bool) {
|
||||
token = newToken
|
||||
username = name
|
||||
isAdmin = admin
|
||||
Keychain.write(newToken, account: keychainAccount)
|
||||
}
|
||||
|
||||
func logout() {
|
||||
token = nil
|
||||
username = ""
|
||||
isAdmin = false
|
||||
Keychain.delete(account: keychainAccount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimaler Keychain-Zugriff für ein einzelnes Token.
|
||||
enum Keychain {
|
||||
private static let service = "com.scarriffle.projectgood"
|
||||
|
||||
static func write(_ value: String, account: String) {
|
||||
delete(account: account)
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecValueData as String: Data(value.utf8),
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
||||
]
|
||||
SecItemAdd(query as CFDictionary, nil)
|
||||
}
|
||||
|
||||
static func read(account: String) -> String? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
var item: CFTypeRef?
|
||||
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
|
||||
let data = item as? Data else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
static func delete(account: String) {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: account,
|
||||
]
|
||||
SecItemDelete(query as CFDictionary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user