1) In den Benachrichtigungs-Regeln liess sich keine Kategorie waehlen: der Abruf
holte den ganzen Snapshot (Produkte + Ablauf + Kategorien) - schlug ein Teil
fehl, kamen auch die Kategorien leer. snapshot ist jetzt widerstandsfaehig
(Teilausfaelle egal), und die Regel-Bearbeitung holt nur noch die Kategorien.
2) Die gruene Erfolgsmeldung ist jetzt eine saubere, eingerueckte Pille mit Titel
("Eingelagert"/"Ausgelagert") und Detailzeile ("Neuer Bestand: 2 Packungen").
Die Einheit steht in der Mehrzahl (Gebinde-Plural vom Server; Basiseinheiten
wie Gramm bleiben unveraendert).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
59 lines
2.7 KiB
Swift
59 lines
2.7 KiB
Swift
import Foundation
|
|
|
|
/// Liest Daten von einem **bestimmten** Server, ohne den aktiven zu wechseln.
|
|
///
|
|
/// `APIClient` ist fest auf das aktive Profil verdrahtet (Session.shared).
|
|
/// Fuer die Benachrichtigungen muss die App aber jeden eingerichteten Server
|
|
/// abfragen - deshalb dieser parallele Pfad, der baseURL und Token ausdruecklich
|
|
/// bekommt. Modelle und Fehlerbedeutung sind dieselben wie im `APIClient`.
|
|
enum ServerFetch {
|
|
/// Alles, was der Planer je Server braucht: ablaufende Chargen im Horizont,
|
|
/// die Produkte (fuer die Kategoriezuordnung) und der Kategorie-Baum.
|
|
struct Snapshot {
|
|
let expiring: [ExpiringItem]
|
|
let products: [Product]
|
|
let categories: [CategoryItem]
|
|
}
|
|
|
|
static func snapshot(baseURL: URL, token: String, horizonDays: Int) async -> Snapshot {
|
|
async let expiring: [ExpiringItem] = get(
|
|
"/expiring?days=\(horizonDays)", baseURL: baseURL, token: token)
|
|
async let products: [Product] = get("/products", baseURL: baseURL, token: token)
|
|
async let categories: [CategoryItem] = get("/categories", baseURL: baseURL, token: token)
|
|
// Einzelne Ausfaelle sollen nicht alles kippen (sonst fehlten z.B. die
|
|
// Kategorien nur, weil die Produktliste langsam war).
|
|
return await Snapshot(
|
|
expiring: (try? await expiring) ?? [],
|
|
products: (try? await products) ?? [],
|
|
categories: (try? await categories) ?? []
|
|
)
|
|
}
|
|
|
|
/// Nur den Kategorie-Baum eines bestimmten Servers holen (fuer die
|
|
/// Regel-Bearbeitung in den Benachrichtigungen).
|
|
static func categories(baseURL: URL, token: String) async throws -> [CategoryItem] {
|
|
try await get("/categories", baseURL: baseURL, token: token)
|
|
}
|
|
|
|
private static func get<T: Decodable>(
|
|
_ path: String, baseURL: URL, token: String
|
|
) async throws -> T {
|
|
guard let url = URL(string: "api" + path, relativeTo: baseURL) else {
|
|
throw APIError.notConfigured
|
|
}
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
// Etwas grosszuegiger als der interaktive Client: Der Abruf laeuft im
|
|
// Hintergrund, ein langsamer Heimserver soll nicht sofort scheitern.
|
|
request.timeoutInterval = 20
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
|
if http.statusCode == 401 { throw APIError.unauthorized }
|
|
throw APIError.server("Serverfehler (\(http.statusCode))")
|
|
}
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
}
|