You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

140 lines
5.9 KiB

import Foundation
import SwiftUI
/// Hält alle Daten und speichert jede Änderung sofort in
/// ~/Library/Application Support/Mawari/daten.json. Einmal am Tag wird eine
/// Sicherungskopie in "Sicherungen" abgelegt (die letzten 60 bleiben).
@MainActor
final class Store: ObservableObject {
@Published var db: Database { didSet { scheduleSave() } }
@Published var lastError: String?
let folder: URL
private var file: URL { folder.appendingPathComponent("daten.json") }
private var backups: URL { folder.appendingPathComponent("Sicherungen") }
private var saveTask: Task<Void, Never>?
init() {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
// MAWARI_DATA_DIR: nur zum Testen, damit echte Daten unberührt bleiben.
if let dir = ProcessInfo.processInfo.environment["MAWARI_DATA_DIR"] {
folder = URL(fileURLWithPath: dir, isDirectory: true)
} else {
folder = base.appendingPathComponent("Mawari", isDirectory: true)
}
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
if let data = try? Data(contentsOf: folder.appendingPathComponent("daten.json")),
let loaded = try? JSONDecoder().decode(Database.self, from: data) {
db = loaded
} else {
db = .empty
}
dailyBackup()
}
// MARK: Speichern
private func scheduleSave() {
saveTask?.cancel()
saveTask = Task { [weak self] in
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
self?.saveNow()
}
}
func saveNow() {
do {
try encoded().write(to: file, options: .atomic)
lastError = nil
} catch {
lastError = "Speichern fehlgeschlagen: \(error.localizedDescription)"
}
}
func encoded() throws -> Data {
var copy = db
copy.updated = ISO8601DateFormatter().string(from: Date())
let enc = JSONEncoder()
enc.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
return try enc.encode(copy)
}
private func dailyBackup() {
guard FileManager.default.fileExists(atPath: file.path) else { return }
try? FileManager.default.createDirectory(at: backups, withIntermediateDirectories: true)
let target = backups.appendingPathComponent("daten-\(Day.today).json")
if !FileManager.default.fileExists(atPath: target.path) {
try? FileManager.default.copyItem(at: file, to: target)
}
let all = (try? FileManager.default.contentsOfDirectory(at: backups, includingPropertiesForKeys: nil)) ?? []
for old in all.filter({ $0.pathExtension == "json" }).sorted(by: { $0.lastPathComponent > $1.lastPathComponent }).dropFirst(60) {
try? FileManager.default.removeItem(at: old)
}
}
// MARK: Import / Sicherung
func importFile(_ url: URL) throws {
let access = url.startAccessingSecurityScopedResource()
defer { if access { url.stopAccessingSecurityScopedResource() } }
let data = try Data(contentsOf: url)
let loaded = try JSONDecoder().decode(Database.self, from: data)
saveNow() // aktuellen Stand vorher sichern
let safety = backups.appendingPathComponent("vor-import-\(Int(Date().timeIntervalSince1970)).json")
try? FileManager.default.createDirectory(at: backups, withIntermediateDirectories: true)
try? FileManager.default.copyItem(at: file, to: safety)
db = loaded
}
// MARK: Abfragen
func task(_ id: String) -> DutyTask? { db.tasks.first { $0.id == id } }
func person(_ id: String) -> Person? { db.persons.first { $0.id == id } }
func name(_ id: String) -> String { person(id)?.name ?? "(gelöscht)" }
var personsSorted: [Person] { db.persons.sorted { $0.name.localizedCompare($1.name) == .orderedAscending } }
func lastDate(person: String, task: DutyTask) -> String? {
var best: String?
for e in db.entries {
let hit = task.partnerColumn ? e.partner == person : (e.task == task.id && e.person == person)
if hit, e.date > (best ?? "") { best = e.date }
}
return best
}
func lastAny(person: String) -> String? {
var best: String?
for e in db.entries where (e.person == person || e.partner == person) && e.date > (best ?? "") { best = e.date }
return best
}
// MARK: Ändern
func save(entry: Entry) {
if let i = db.entries.firstIndex(where: { $0.id == entry.id }) { db.entries[i] = entry } else { db.entries.append(entry) }
}
func delete(entry id: String) { db.entries.removeAll { $0.id == id } }
func addPerson(named name: String) -> Bool {
let n = name.trimmingCharacters(in: .whitespaces)
guard !n.isEmpty, !db.persons.contains(where: { $0.name.lowercased() == n.lowercased() }) else { return false }
db.persons.append(Person(id: newID("p"), name: n))
return true
}
func deletePerson(_ id: String) {
db.entries.removeAll { $0.person == id }
for i in db.entries.indices where db.entries[i].partner == id { db.entries[i].partner = "" }
db.persons.removeAll { $0.id == id }
}
func entryCount(person id: String) -> Int { db.entries.filter { $0.person == id || $0.partner == id }.count }
func entryCount(task: DutyTask) -> Int { task.partnerColumn ? db.entries.filter { !$0.partner.isEmpty }.count : db.entries.filter { $0.task == task.id }.count }
func binding(person id: String) -> Binding<Person>? {
guard db.persons.contains(where: { $0.id == id }) else { return nil }
return Binding(
get: { self.db.persons.first { $0.id == id } ?? Person(id: id, name: "") },
set: { new in if let i = self.db.persons.firstIndex(where: { $0.id == id }) { self.db.persons[i] = new } })
}
}

Powered by TurnKey Linux.