// // Schema.swift // HaruDanim // // SwiftData schema definitions: models, enums, and relationships. // import Foundation import SwiftData // MARK: - Enums /// Determines how an `Action` is tracked. enum ActionType: String, Codable, CaseIterable { /// Tracked via elapsed time (`TimeSession`). case time /// Tracked via discrete counts (`CountEntry`). case count } /// Lifecycle state of a `Goal`. enum GoalStatus: String, Codable, CaseIterable { case inProgress case achieved case failed /// Awaiting user confirmation (e.g. period ended, result ambiguous). case needsConfirmation } /// The recurrence window a `Quest` is evaluated over. enum QuestPeriod: String, Codable, CaseIterable { case daily case weekly case monthly case custom } /// Whether the target value should be met by going above or below it. enum QuestDirection: String, Codable, CaseIterable { /// Succeed when the measured value is at or above the target. case above /// Succeed when the measured value is at or below the target. case below } // MARK: - Tag @Model final class Tag { @Attribute(.unique) var id: UUID var name: String /// Hex color string (e.g. `#2F6B4F`) for tag styling. var colorHex: String var createdAt: Date /// Actions carrying this tag (many-to-many). Inverse declared on `Action.tags`. var actions: [Action] init( id: UUID = UUID(), name: String, colorHex: String = "#2F6B4F", createdAt: Date = .now, actions: [Action] = [] ) { self.id = id self.name = name self.colorHex = colorHex self.createdAt = createdAt self.actions = actions } } // MARK: - Action @Model final class Action { @Attribute(.unique) var id: UUID var name: String var typeRaw: String /// SF Symbol name used for iconography. var iconName: String var createdAt: Date /// Tags applied to this action (many-to-many, owning side of the inverse). @Relationship(inverse: \Tag.actions) var tags: [Tag] /// Time-based tracking records. Deleting the action cascades to its sessions. @Relationship(deleteRule: .cascade, inverse: \TimeSession.action) var timeSessions: [TimeSession] /// Count-based tracking records. Deleting the action cascades to its entries. @Relationship(deleteRule: .cascade, inverse: \CountEntry.action) var countEntries: [CountEntry] var type: ActionType { get { ActionType(rawValue: typeRaw) ?? .time } set { typeRaw = newValue.rawValue } } init( id: UUID = UUID(), name: String, type: ActionType = .time, iconName: String = "circle", createdAt: Date = .now, tags: [Tag] = [], timeSessions: [TimeSession] = [], countEntries: [CountEntry] = [] ) { self.id = id self.name = name self.typeRaw = type.rawValue self.iconName = iconName self.createdAt = createdAt self.tags = tags self.timeSessions = timeSessions self.countEntries = countEntries } } // MARK: - TimeSession @Model final class TimeSession { @Attribute(.unique) var id: UUID var startTime: Date /// `nil` while the session is still running. var endTime: Date? /// The action this session belongs to (inverse of `Action.timeSessions`). var action: Action? init( id: UUID = UUID(), startTime: Date = .now, endTime: Date? = nil, action: Action? = nil ) { self.id = id self.startTime = startTime self.endTime = endTime self.action = action } } // MARK: - CountEntry @Model final class CountEntry { @Attribute(.unique) var id: UUID var timestamp: Date /// Number of units recorded in this entry. var amount: Int /// The action this entry belongs to (inverse of `Action.countEntries`). var action: Action? init( id: UUID = UUID(), timestamp: Date = .now, amount: Int = 1, action: Action? = nil ) { self.id = id self.timestamp = timestamp self.amount = amount self.action = action } } // MARK: - Goal @Model final class Goal { @Attribute(.unique) var id: UUID var title: String var statusRaw: String var createdAt: Date /// Quests composing this goal. Deleting the goal cascades to its quests. @Relationship(deleteRule: .cascade, inverse: \Quest.goal) var quests: [Quest] var status: GoalStatus { get { GoalStatus(rawValue: statusRaw) ?? .inProgress } set { statusRaw = newValue.rawValue } } init( id: UUID = UUID(), title: String, status: GoalStatus = .inProgress, createdAt: Date = .now, quests: [Quest] = [] ) { self.id = id self.title = title self.statusRaw = status.rawValue self.createdAt = createdAt self.quests = quests } } // MARK: - Quest @Model final class Quest { @Attribute(.unique) var id: UUID var title: String var periodRaw: String var directionRaw: String /// The threshold this quest is measured against (seconds for time, units for count). var targetValue: Double var createdAt: Date /// The goal this quest belongs to (inverse of `Goal.quests`). var goal: Goal? var period: QuestPeriod { get { QuestPeriod(rawValue: periodRaw) ?? .daily } set { periodRaw = newValue.rawValue } } var direction: QuestDirection { get { QuestDirection(rawValue: directionRaw) ?? .above } set { directionRaw = newValue.rawValue } } init( id: UUID = UUID(), title: String, period: QuestPeriod = .daily, direction: QuestDirection = .above, targetValue: Double = 0, createdAt: Date = .now, goal: Goal? = nil ) { self.id = id self.title = title self.periodRaw = period.rawValue self.directionRaw = direction.rawValue self.targetValue = targetValue self.createdAt = createdAt self.goal = goal } } // MARK: - AppSettings @Model final class AppSettings { @Attribute(.unique) var id: UUID /// Hour (0...23) at which a new logical day begins. Default 00:00. var dayStartHour: Int init( id: UUID = UUID(), dayStartHour: Int = 0 ) { self.id = id self.dayStartHour = dayStartHour } }