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.

146 lines
6.0 KiB

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() }

Powered by TurnKey Linux.