66128e7a5c
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
173 lines
6.9 KiB
Swift
173 lines
6.9 KiB
Swift
import SwiftUI
|
|
|
|
/// "Just this week" (premium): adds the weekend or turns meals on/off for a
|
|
/// single week without touching the global settings. Removing a day or a meal
|
|
/// deletes its slots, so anything already planned there is confirmed first.
|
|
struct WeekScheduleSheet: View {
|
|
let plan: WeekPlan
|
|
let settings: AppSettings
|
|
let onApply: (_ includeWeekends: Bool, _ mealTypes: [MealType]) -> Void
|
|
let onReset: () -> Void
|
|
let onUpsell: () -> Void
|
|
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var includeWeekends: Bool
|
|
@State private var selectedMealTypes: Set<MealType>
|
|
@State private var showDestructiveConfirm: Bool = false
|
|
|
|
private var canEdit: Bool {
|
|
PremiumAccess.canCustomizeWeekSchedule(isPremium: settings.isPremium)
|
|
}
|
|
|
|
init(
|
|
plan: WeekPlan,
|
|
settings: AppSettings,
|
|
onApply: @escaping (_ includeWeekends: Bool, _ mealTypes: [MealType]) -> Void,
|
|
onReset: @escaping () -> Void,
|
|
onUpsell: @escaping () -> Void
|
|
) {
|
|
self.plan = plan
|
|
self.settings = settings
|
|
self.onApply = onApply
|
|
self.onReset = onReset
|
|
self.onUpsell = onUpsell
|
|
|
|
let schedule = settings.schedule(for: plan)
|
|
_includeWeekends = State(initialValue: schedule.includeWeekends)
|
|
_selectedMealTypes = State(initialValue: Set(schedule.mealTypes))
|
|
}
|
|
|
|
/// Slots that would be deleted by the current selection because their day
|
|
/// or meal is no longer planned — and that already hold something.
|
|
private var slotsAtRisk: [MealSlot] {
|
|
let lastDay = includeWeekends ? 6 : 4
|
|
return plan.slotList.filter { slot in
|
|
let dropped = slot.dayOfWeek > lastDay || !selectedMealTypes.contains(slot.mealTypeEnum)
|
|
let hasContent = slot.dishId != nil || slot.isEatingOut || slot.isSkipped
|
|
return dropped && hasContent
|
|
}
|
|
}
|
|
|
|
private var hasChanges: Bool {
|
|
let current = settings.schedule(for: plan)
|
|
return includeWeekends != current.includeWeekends || selectedMealTypes != Set(current.mealTypes)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
Text("week_schedule_intro")
|
|
.font(.mealMoodCaption)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
if !canEdit {
|
|
PremiumUpsellBanner(
|
|
messageKey: "week_schedule_premium_message",
|
|
actionTitleKey: "premium_subscribe",
|
|
action: onUpsell
|
|
)
|
|
.listRowInsets(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
|
}
|
|
} header: {
|
|
Text(plan.weekStartDate.formattedWeekRange())
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
Section {
|
|
ForEach(MealType.allCases, id: \.self) { mealType in
|
|
Toggle(isOn: Binding(
|
|
get: { selectedMealTypes.contains(mealType) },
|
|
set: { isOn in
|
|
if isOn {
|
|
selectedMealTypes.insert(mealType)
|
|
} else if selectedMealTypes.count > 1 {
|
|
selectedMealTypes.remove(mealType)
|
|
}
|
|
}
|
|
)) {
|
|
Label {
|
|
Text(String(localized: String.LocalizationValue(mealType.localizedKey)))
|
|
} icon: {
|
|
Image(systemName: mealType.icon)
|
|
.foregroundStyle(Color.mealMoodCoral)
|
|
}
|
|
}
|
|
.disabled(!canEdit)
|
|
}
|
|
} header: {
|
|
Text("week_schedule_meals_section")
|
|
} footer: {
|
|
Text("week_schedule_meals_footer")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
Section {
|
|
Toggle(isOn: $includeWeekends) {
|
|
Label {
|
|
Text("settings_include_weekends")
|
|
} icon: {
|
|
Image(systemName: "calendar")
|
|
.foregroundStyle(Color.mealMoodCoral)
|
|
}
|
|
}
|
|
.disabled(!canEdit)
|
|
} header: {
|
|
Text("week_schedule_days_section")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
if plan.hasScheduleOverride {
|
|
Section {
|
|
Button(role: .destructive) {
|
|
onReset()
|
|
dismiss()
|
|
} label: {
|
|
Label("week_schedule_reset", systemImage: "arrow.counterclockwise")
|
|
}
|
|
.disabled(!canEdit)
|
|
} footer: {
|
|
Text("week_schedule_reset_footer")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color.mealMoodBackground.ignoresSafeArea())
|
|
.tint(.mealMoodCoral)
|
|
.navigationTitle("week_schedule_title")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("reset_cancel") { dismiss() }
|
|
}
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("week_schedule_apply") {
|
|
if slotsAtRisk.isEmpty {
|
|
apply()
|
|
} else {
|
|
showDestructiveConfirm = true
|
|
}
|
|
}
|
|
.disabled(!canEdit || !hasChanges)
|
|
}
|
|
}
|
|
.alert("week_schedule_warning_title", isPresented: $showDestructiveConfirm) {
|
|
Button("reset_cancel", role: .cancel) {}
|
|
Button("week_schedule_warning_confirm", role: .destructive) { apply() }
|
|
} message: {
|
|
Text(String(format: String(localized: "week_schedule_warning_message"), slotsAtRisk.count))
|
|
}
|
|
}
|
|
// Full height: at medium the weekend toggle and the reset button fall
|
|
// below the fold, which is exactly what the sheet is for.
|
|
.presentationDetents([.large])
|
|
.presentationDragIndicator(.visible)
|
|
}
|
|
|
|
private func apply() {
|
|
onApply(includeWeekends, MealType.allCases.filter { selectedMealTypes.contains($0) })
|
|
dismiss()
|
|
}
|
|
}
|