import Foundation // Datenformat ist bewusst identisch mit dem HTML-Tool (leben-und-dienst.json), // damit dessen Dateien direkt importiert und Sicherungen dort geöffnet werden können. enum TaskGroup: String, Codable, CaseIterable, Identifiable { case diener, schulung var id: String { rawValue } var title: String { self == .diener ? "Aufgaben für Diener (Ernannte)" : "Schulungsaufgaben" } var shortTitle: String { self == .diener ? "Diener (Ernannte)" : "Schulung" } } struct DutyTask: Codable, Identifiable, Hashable { var id: String var name: String var group: TaskGroup var withPartner: Bool = false var isPartner: Bool? = nil var partnerColumn: Bool { isPartner == true } } struct Person: Codable, Identifiable, Hashable { var id: String var name: String var gender: String = "" var note: String = "" var tasks: [String] = [] func may(_ taskID: String) -> Bool { tasks.contains(taskID) } } struct Entry: Codable, Identifiable, Hashable { var id: String var date: String // YYYY-MM-DD var task: String var person: String var partner: String = "" var note: String = "" } struct Database: Codable { var app = "leben-und-dienst-uebersicht" var version = 1 var updated = "" var tasks: [DutyTask] var persons: [Person] var entries: [Entry] static let defaultTasks: [DutyTask] = ([ ("vorsitz", "Vorsitz", .diener, false), ("gebet", "Gebet", .diener, false), ("schaetze", "Schätze aus Gottes Wort", .diener, false), ("graben", "Schätze graben", .diener, false), ("leben", "Leben als Christ", .diener, false), ("vbs-leitung", "VBS-Leitung", .diener, false), ("vbs-leser", "VBS-Leser", .diener, false), ("bibellesung", "Bibellesung", .schulung, false), ("gespraeche", "Gespräche beginnen", .schulung, true), ("interesse", "Interesse fördern", .schulung, true), ("juenger", "Menschen zu Jüngern machen", .schulung, true), ("glauben", "Glaubensansichten erklären", .schulung, true), ("sagen", "Was würdest du sagen?", .schulung, true), ("vortrag", "Vortrag", .schulung, false), ("partner", "Partner", .schulung, false), ] as [(String, String, TaskGroup, Bool)]).map { DutyTask(id: $0.0, name: $0.1, group: $0.2, withPartner: $0.3, isPartner: $0.0 == "partner" ? true : nil) } static var empty: Database { Database(tasks: defaultTasks, persons: [], entries: []) } // Tolerant gegenüber Dateien des HTML-Tools mit fehlenden Feldern. init(tasks: [DutyTask], persons: [Person], entries: [Entry]) { self.tasks = tasks; self.persons = persons; self.entries = entries } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) app = try c.decodeIfPresent(String.self, forKey: .app) ?? app version = try c.decodeIfPresent(Int.self, forKey: .version) ?? 1 updated = try c.decodeIfPresent(String.self, forKey: .updated) ?? "" let t = try c.decodeIfPresent([DutyTask].self, forKey: .tasks) ?? [] tasks = t.isEmpty ? Database.defaultTasks : t persons = try c.decode([Person].self, forKey: .persons) entries = try c.decode([Entry].self, forKey: .entries) } } extension Person { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) name = try c.decode(String.self, forKey: .name) gender = try c.decodeIfPresent(String.self, forKey: .gender) ?? "" note = try c.decodeIfPresent(String.self, forKey: .note) ?? "" tasks = try c.decodeIfPresent([String].self, forKey: .tasks) ?? [] } } extension Entry { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) date = try c.decode(String.self, forKey: .date) task = try c.decode(String.self, forKey: .task) person = try c.decode(String.self, forKey: .person) partner = try c.decodeIfPresent(String.self, forKey: .partner) ?? "" note = try c.decodeIfPresent(String.self, forKey: .note) ?? "" } } extension DutyTask { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) name = try c.decode(String.self, forKey: .name) group = (try? c.decode(TaskGroup.self, forKey: .group)) ?? .schulung withPartner = try c.decodeIfPresent(Bool.self, forKey: .withPartner) ?? false isPartner = try c.decodeIfPresent(Bool.self, forKey: .isPartner) } } // MARK: - Datum enum Day { nonisolated(unsafe) private static let iso: DateFormatter = { let f = DateFormatter(); f.calendar = Calendar(identifier: .gregorian) f.locale = Locale(identifier: "en_US_POSIX"); f.timeZone = .current; f.dateFormat = "yyyy-MM-dd"; return f }() nonisolated(unsafe) private static let short: DateFormatter = { let f = DateFormatter(); f.locale = Locale(identifier: "de_DE"); f.dateFormat = "dd.MM.yy"; return f }() static func string(_ d: Date) -> String { iso.string(from: d) } static func date(_ s: String) -> Date? { iso.date(from: s) } static var today: String { string(Date()) } static func display(_ s: String) -> String { date(s).map { short.string(from: $0) } ?? s } static func weeksAgo(_ s: String) -> Int? { guard let d = date(s), let t = date(today) else { return nil } let days = Calendar.current.dateComponents([.day], from: d, to: t).day ?? 0 return days < 0 ? -1 : days / 7 } static func agoText(_ s: String) -> String { switch weeksAgo(s) { case .none: return "" case .some(let w) where w < 0: return "geplant" case 0: return "diese Woche" case 1: return "vor 1 Woche" case .some(let w): return "vor \(w) Wochen" } } } func newID(_ prefix: String) -> String { prefix + UUID().uuidString.prefix(8).lowercased() }