premium: configurar días y comidas de una semana concreta

Hasta ahora includeWeekends y activeMealTypes vivían solo en AppSettings y
aplicaban a todo el calendario. WeekPlan puede ahora sobreescribirlos para su
semana (includeWeekendsOverride / mealTypesOverrideRaw) y WeekSchedule resuelve
qué manda en cada semana: su override si lo tiene, los ajustes globales si no.

Todo lo que pintaba días y comidas —planificador, exports, compartir texto,
widget, watch— pasa por settings.schedule(for:), así que una semana con fin de
semana añadido o con desayuno activo se ve igual en todas partes.

Crear o editar el override es premium; los ya guardados se respetan aunque la
suscripción caduque, para no borrar comidas ya planificadas. La hoja avisa y
pide confirmación cuando quitar un día o una comida se lleva por delante algo
ya planificado.

Refs #32

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
This commit is contained in:
alexandrev-tibco
2026-09-12 12:05:34 +02:00
parent e58227a113
commit 66128e7a5c
21 changed files with 777 additions and 17 deletions
+37
View File
@@ -12,6 +12,13 @@ final class WeekPlan {
var updatedAt: Date = Date()
var userRating: Int = 0 // 0 = unrated, 1 = liked, -1 = disliked
/// Per-week overrides of the global schedule (2.1.1, premium). nil = follow
/// `AppSettings`. Optional so existing stores lightweight-migrate and
/// CloudKit accepts the new attributes.
var includeWeekendsOverride: Bool?
/// Comma-separated MealType raw values, chronological. nil = follow settings.
var mealTypesOverrideRaw: String?
@Relationship(deleteRule: .cascade)
var slots: [MealSlot]? = []
@@ -21,6 +28,36 @@ final class WeekPlan {
set { slots = newValue }
}
/// The meal types this week overrides the global setting with, or nil when
/// it follows `AppSettings.activeMealTypes`.
var mealTypesOverride: [MealType]? {
get {
guard let raw = mealTypesOverrideRaw else { return nil }
let types = raw.split(separator: ",").compactMap { MealType(rawValue: String($0)) }
guard !types.isEmpty else { return nil }
return MealType.allCases.filter { types.contains($0) }
}
set {
guard let newValue else {
mealTypesOverrideRaw = nil
return
}
let ordered = MealType.allCases.filter { newValue.contains($0) }
// An empty selection would leave the week with no slots at all;
// treat it as "no override" instead.
mealTypesOverrideRaw = ordered.isEmpty ? nil : ordered.map(\.rawValue).joined(separator: ",")
}
}
var hasScheduleOverride: Bool {
includeWeekendsOverride != nil || mealTypesOverrideRaw != nil
}
func clearScheduleOverride() {
includeWeekendsOverride = nil
mealTypesOverrideRaw = nil
}
init(
id: UUID = UUID(),
weekStartDate: Date,