1.1.0 feature work: Monthly Check-in, Charts, Goals, Share, Reviews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-11 23:37:11 +01:00
parent daaca95913
commit 4761e2e5c8
19 changed files with 1118 additions and 90 deletions
@@ -5,6 +5,7 @@ enum MonthlyCheckInStore {
private static let completionsKey = "monthlyCheckInCompletions"
private static let legacyLastCheckInKey = "lastCheckInDate"
private static let entriesKey = "monthlyCheckInEntries"
private static let graceDays = 20
// MARK: - Public Accessors
@@ -44,7 +45,7 @@ enum MonthlyCheckInStore {
}
static func monthKey(for date: Date) -> String {
Self.monthFormatter.string(from: date)
Self.monthFormatter.string(from: effectiveMonth(for: date))
}
static func allNotes() -> [(date: Date, note: String)] {
@@ -74,11 +75,42 @@ enum MonthlyCheckInStore {
}
static func setCompletionDate(_ completionDate: Date, for month: Date) {
let targetMonth = month.startOfMonth
let targetMonth = effectiveMonth(for: month, relativeTo: completionDate, graceDays: graceDays)
let targetKey = monthKey(for: targetMonth)
var entries = loadEntries()
var didChange = false
let calendar = Calendar.current
if calendar.isDate(month, inSameDayAs: completionDate),
calendar.component(.day, from: completionDate) > graceDays {
let completedEntries = entries.compactMap { key, entry -> (month: Date, entry: MonthlyCheckInEntry)? in
guard let monthDate = monthFormatter.date(from: key)?.startOfMonth else { return nil }
guard entry.completionTime != nil else { return nil }
guard monthDate < targetMonth else { return nil }
return (month: monthDate, entry: entry)
}
if let lastCompleted = completedEntries.max(by: { $0.month < $1.month }) {
var cursor = lastCompleted.month.adding(months: 1).startOfMonth
while cursor < targetMonth {
let key = monthFormatter.string(from: cursor)
if entries[key] == nil {
let fallbackCompletion = min(cursor.endOfMonth, completionDate)
let entry = MonthlyCheckInEntry(
note: lastCompleted.entry.note,
rating: lastCompleted.entry.rating,
mood: lastCompleted.entry.mood,
completionTime: fallbackCompletion.timeIntervalSince1970,
createdAt: Date().timeIntervalSince1970
)
entries[key] = entry
didChange = true
}
cursor = cursor.adding(months: 1).startOfMonth
}
}
}
// Ensure the target month entry exists.
var targetEntry = entries[targetKey] ?? MonthlyCheckInEntry(
note: nil,
@@ -123,6 +155,21 @@ enum MonthlyCheckInStore {
return Date(timeIntervalSince1970: legacy)
}
static func effectiveMonth(
for date: Date,
relativeTo referenceDate: Date = Date(),
graceDays: Int = 20
) -> Date {
let calendar = Calendar.current
if calendar.isDate(date, inSameDayAs: referenceDate) {
let day = calendar.component(.day, from: referenceDate)
if day <= graceDays {
return referenceDate.adding(months: -1).startOfMonth
}
}
return date.startOfMonth
}
static func stats(referenceDate: Date = Date()) -> MonthlyCheckInStats {
let cutoff = referenceDate.endOfMonth
let entries = allEntries().filter { $0.date <= cutoff }