import Foundation // MARK: - Suggestion Feedback enum SuggestionFeedback: String, Codable { case didIt // "Success!" then any reflection case skipped // "Next Time" case dismissed // "Don't Suggest" (removed from pool) } enum WellnessReflection: String, Codable { case great // "Feeling Great" case same // "About the Same" case notReally // "Not Really" } // MARK: - Work Log Entry struct WorkLogEntry: Codable, Identifiable { let id: UUID let date: Date let durationSeconds: Int let intent: String? let completed: Bool let reflection: String? let additionalNotes: String? let suggestion: String? let suggestionFeedback: SuggestionFeedback? let wellnessReflection: WellnessReflection? init( id: UUID = UUID(), date: Date, durationSeconds: Int, intent: String?, completed: Bool, reflection: String?, additionalNotes: String?, suggestion: String? = nil, suggestionFeedback: SuggestionFeedback? = nil, wellnessReflection: WellnessReflection? = nil ) { self.id = id self.date = date self.durationSeconds = durationSeconds self.intent = intent self.completed = completed self.reflection = reflection self.additionalNotes = additionalNotes self.suggestion = suggestion self.suggestionFeedback = suggestionFeedback self.wellnessReflection = wellnessReflection } } // MARK: - Work Log Store class WorkLogStore { static let shared = WorkLogStore() private let key = "pommedoro.workLog" private let defaults = UserDefaults.standard private let encoder = JSONEncoder() private let decoder = JSONDecoder() private var lastLoggedDay: String? private(set) var entries: [WorkLogEntry] = [] private static var workLogFileURL: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Documents", isDirectory: true) .appendingPathComponent("pommedoro", isDirectory: true) .appendingPathComponent("worklog.log", isDirectory: false) } private init() { load() clearOldEntries() } func addEntry(_ entry: WorkLogEntry) { entries.append(entry) save() appendEntryToFile(entry) } func todayEntries() -> [WorkLogEntry] { let calendar = Calendar.current return entries.filter { calendar.isDateInToday($0.date) } } func lastTodayIntent() -> String? { return todayEntries().last(where: { $0.intent != nil && !$0.intent!.isEmpty })?.intent } func todaySummary() -> (intervals: Int, totalSeconds: Int) { let today = todayEntries() let total = today.reduce(0) { $0 + $1.durationSeconds } return (intervals: today.count, totalSeconds: total) } func todayLogAsText() -> String { formatEntriesAsText(todayEntries(), includeDaySeparator: false) } private func save() { guard let data = try? encoder.encode(entries) else { return } defaults.set(data, forKey: key) } private func load() { guard let data = defaults.data(forKey: key), let decoded = try? decoder.decode([WorkLogEntry].self, from: data) else { entries = [] return } entries = decoded } func clearOldEntries() { let cutoff = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date() let before = entries.count entries.removeAll { $0.date < cutoff } if entries.count != before { save() } } private func ensureWorkLogDirectory() { let dir = Self.workLogFileURL.deletingLastPathComponent() try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } private static let isoFormatter: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] f.timeZone = TimeZone.current return f }() private static let dayFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd" f.timeZone = TimeZone.current return f }() private func appendEntryToFile(_ entry: WorkLogEntry) { ensureWorkLogDirectory() let dayStr = Self.dayFormatter.string(from: entry.date) var lines: [String] = [] if lastLoggedDay != dayStr { if lastLoggedDay != nil { lines.append("") } lines.append("--- \(dayStr) ---") lastLoggedDay = dayStr } let iso = Self.isoFormatter.string(from: entry.date) let durMins = entry.durationSeconds / 60 let status = entry.completed ? "completed" : "partial" let intentPart = entry.intent.map { " intent: \(flattenForLog($0))" } ?? "" let reflPart = entry.reflection.map { " reflection: \(flattenForLog($0))" } ?? "" let suggPart = entry.suggestion.map { " suggestion: \(flattenForLog($0))" } ?? "" let fbPart = entry.suggestionFeedback.map { " feedback: \($0.rawValue)" } ?? "" let wellPart = entry.wellnessReflection.map { " wellness: \($0.rawValue)" } ?? "" lines.append("\(iso) \(durMins)m \(status)\(intentPart)\(reflPart)\(suggPart)\(fbPart)\(wellPart)") guard let data = (lines.joined(separator: "\n") + "\n").data(using: .utf8) else { return } if let handle = try? FileHandle(forWritingTo: Self.workLogFileURL) { handle.seekToEndOfFile() handle.write(data) try? handle.close() } else { try? data.write(to: Self.workLogFileURL) } } private func flattenForLog(_ s: String) -> String { s.replacingOccurrences(of: "\n", with: " ") .replacingOccurrences(of: "\t", with: " ") .trimmingCharacters(in: .whitespaces) } private func formatEntriesAsText(_ entries: [WorkLogEntry], includeDaySeparator: Bool) -> String { guard !entries.isEmpty else { return "No intervals logged yet today." } var parts: [String] = [] var currentDay: String? for entry in entries { let dayStr = Self.dayFormatter.string(from: entry.date) if includeDaySeparator, currentDay != dayStr { if currentDay != nil { parts.append("") } parts.append("--- \(dayStr) ---") currentDay = dayStr } else if !includeDaySeparator { currentDay = dayStr } let iso = Self.isoFormatter.string(from: entry.date) let durMins = entry.durationSeconds / 60 let status = entry.completed ? "completed" : "partial" var line = "\(iso) \(durMins)m \(status)" if let i = entry.intent, !i.isEmpty { line += " intent: \(flattenForLog(i))" } if let r = entry.reflection, !r.isEmpty { line += " reflection: \(flattenForLog(r))" } if let s = entry.suggestion, !s.isEmpty { line += " suggestion: \(flattenForLog(s))" } if let fb = entry.suggestionFeedback { line += " feedback: \(fb.rawValue)" } if let w = entry.wellnessReflection { line += " wellness: \(w.rawValue)" } parts.append(line) } return parts.joined(separator: "\n") } }