iOS: Ablauf-Benachrichtigungen, je Server einstellbar
Die App zeigte "Bald ablaufend" nur beim Oeffnen. Jetzt erinnert sie aktiv vor dem Ablauf - mit frei zusammenstellbaren Regeln, pro Server getrennt. Eine Regel ist "X Tage/Wochen vor Ablauf", optional nur fuer eine Kategorie (inklusive deren Unterkategorien), einmalig oder taeglich wiederkehrend. Eine Kategorie-Regel ersetzt fuer ihre Produkte die allgemeine - so kann Molkerei frueher warnen als Chips. Dazu am Ablauftag eine letzte, deutliche Warnung fuer alle Produkte (eigener Schalter). Die Meldungen kommen als Tages-Sammelmeldung, je Server eine, zur pro Server eingestellten Uhrzeit. Weil Vorrania im Heimnetz laeuft und von unterwegs oft nicht erreichbar ist, kann die App nicht zuverlaessig im Hintergrund abfragen - und iOS verlangt ohnehin, dass lokale Benachrichtigungen im Voraus geplant werden. Also: aus den Ablaufdaten kuenftige Termine berechnen und einplanen; aufgefrischt beim Oeffnen, im Vordergrund und best-effort per Hintergrund-Aktualisierung. Geplant wird fuer alle eingerichteten Server, jeder mit seinem Token aus dem Keychain; unerreichbare werden uebersprungen. iOS erlaubt 64 ausstehende Meldungen je App, deshalb die naechstliegenden zuerst und bei 60 gekappt. Einstellungen sitzen hinter der Glocke je Server in der Server-Verwaltung (nicht admin-beschraenkt, es ist eine Geraete-Einstellung). Getippt wechselt die Meldung auf den betroffenen Server und oeffnet "Bald ablaufend". Keine Backend-Aenderung: die Kategorie kommt aus /products, der Ablauf aus /expiring. Die Rechenlogik (ExpiryPlanner) ist bewusst netz- und iOS-frei und mit einem eigenstaendigen Swift-Harnisch geprueft: Kategorie-Vorrang, Unterkategorien, Rueckfall auf die allgemeine Regel, einmalig vs. wiederkehrend und die letzte Warnung. Modelle gegen echte /expiring- und /products-Antworten abgeglichen, App startet ohne Absturz (auch die BGTask-Registrierung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
86
ios/Sources/NotificationSettings.swift
Normal file
86
ios/Sources/NotificationSettings.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
|
||||
/// Einstellungen fuer Ablauf-Benachrichtigungen - je Server eigene.
|
||||
///
|
||||
/// Rein auf dem Geraet gespeichert (UserDefaults), nicht am Server: Es ist eine
|
||||
/// Einstellung dieser App auf diesem iPhone, kein geteilter Serverzustand.
|
||||
|
||||
/// Die Einheit, in der „X vor Ablauf" gemeint ist.
|
||||
enum LeadUnit: String, Codable, CaseIterable {
|
||||
case days, weeks
|
||||
|
||||
var label: String { self == .weeks ? "Wochen" : "Tage" }
|
||||
}
|
||||
|
||||
/// Eine Erinnerungsregel: „X Tage/Wochen vor Ablauf", optional nur fuer eine
|
||||
/// Kategorie, einmalig oder taeglich wiederkehrend.
|
||||
struct NotificationRule: Codable, Identifiable, Equatable {
|
||||
let id: UUID
|
||||
var amount: Int
|
||||
var unit: LeadUnit
|
||||
/// nil = alle Kategorien (die allgemeine Regel).
|
||||
var categoryId: Int?
|
||||
/// true = das Produkt taucht jeden Tag im Fenster (ab „X vorher" bis Ablauf)
|
||||
/// in der Tagesmeldung auf; false = nur am Tag „X vorher".
|
||||
var recurring: Bool
|
||||
|
||||
init(id: UUID = UUID(), amount: Int = 3, unit: LeadUnit = .days,
|
||||
categoryId: Int? = nil, recurring: Bool = false) {
|
||||
self.id = id
|
||||
self.amount = amount
|
||||
self.unit = unit
|
||||
self.categoryId = categoryId
|
||||
self.recurring = recurring
|
||||
}
|
||||
|
||||
/// Vorlaufzeit in Tagen - Wochen werden umgerechnet.
|
||||
var leadDays: Int { unit == .weeks ? amount * 7 : amount }
|
||||
}
|
||||
|
||||
/// Alle Benachrichtigungseinstellungen eines Servers.
|
||||
struct ServerNotificationConfig: Codable, Equatable {
|
||||
var enabled: Bool = false
|
||||
/// Uhrzeit der Tagesmeldung.
|
||||
var hour: Int = 9
|
||||
var minute: Int = 0
|
||||
/// Letzte Warnung am Ablauftag („ACHTUNG: … abgelaufen"), fuer alle Produkte.
|
||||
var lastWarningOnExpiry: Bool = true
|
||||
var rules: [NotificationRule] = []
|
||||
|
||||
/// Ohne aktive Regel und ohne letzte Warnung gibt es nichts zu planen.
|
||||
var hasSomethingToSchedule: Bool {
|
||||
enabled && (!rules.isEmpty || lastWarningOnExpiry)
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest und schreibt die Konfigurationen. Ein Woerterbuch je Profil-UUID,
|
||||
/// analog zu `ProfileStore`.
|
||||
enum NotificationStore {
|
||||
static let key = "notification_configs"
|
||||
|
||||
static func loadAll() -> [String: ServerNotificationConfig] {
|
||||
guard let data = UserDefaults.standard.data(forKey: key),
|
||||
let dict = try? JSONDecoder().decode([String: ServerNotificationConfig].self, from: data)
|
||||
else { return [:] }
|
||||
return dict
|
||||
}
|
||||
|
||||
static func config(for profileID: UUID) -> ServerNotificationConfig {
|
||||
loadAll()[profileID.uuidString] ?? ServerNotificationConfig()
|
||||
}
|
||||
|
||||
static func save(_ config: ServerNotificationConfig, for profileID: UUID) {
|
||||
var alle = loadAll()
|
||||
alle[profileID.uuidString] = config
|
||||
guard let data = try? JSONEncoder().encode(alle) else { return }
|
||||
UserDefaults.standard.set(data, forKey: key)
|
||||
}
|
||||
|
||||
/// Beim Loeschen eines Profils die zugehoerige Konfiguration mit entfernen.
|
||||
static func remove(for profileID: UUID) {
|
||||
var alle = loadAll()
|
||||
alle.removeValue(forKey: profileID.uuidString)
|
||||
guard let data = try? JSONEncoder().encode(alle) else { return }
|
||||
UserDefaults.standard.set(data, forKey: key)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user