1.0.5: Eating out slots, rule violations panel, Crashlytics, search bar fix

- Eating out: tap any empty slot → "Mark as eating out"; shows teal indicator,
  skipped by auto-assign, counts as complete, tap again to unmark
- Rule violations panel: access via wand long-press context menu when conflicts exist;
  shows all isRuleOverridden slots with Fix (clear) or Ignore (acknowledge) actions
- Firebase Crashlytics integrated: CrashlyticsService + dSYM upload build phase,
  isPremium property tracked per session
- PremiumSyncService: extracted premium state machine, StoreKit Transaction.updates
  listener, isPremium no longer synced via iCloud to avoid stale state
- StoreManager: analytics on purchase/restore, bundle ID fallback for product ID lookup
- Search bar contrast bug fixed: TextField now has explicit foreground color for dark mode
- WelcomeStepView: redesigned onboarding welcome screen with week preview
- Version bump: 1.0.5 build 22

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-04-27 09:48:20 +02:00
parent 6c7e12b41f
commit 110807d239
111 changed files with 3116 additions and 154 deletions
+35 -4
View File
@@ -12,6 +12,7 @@ final class HomeViewModel: ObservableObject {
private struct SlotSnapshot {
let dishId: UUID?
let isRuleOverridden: Bool
let isEatingOut: Bool
}
@Published var currentWeekStart: Date
@@ -116,7 +117,7 @@ final class HomeViewModel: ObservableObject {
captureUndoSnapshot(plan: plan)
isAutoCompleting = true
let emptySlots = plan.slots.filter { $0.dishId == nil }
let emptySlots = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }
let result = AutocompleteEngine.autocomplete(
emptySlots: emptySlots,
currentPlan: plan,
@@ -160,6 +161,7 @@ final class HomeViewModel: ObservableObject {
}
slot.dishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
}
plan.updatedAt = Date()
AnalyticsService.logWeekReset()
@@ -297,9 +299,11 @@ final class HomeViewModel: ObservableObject {
if let snap = lastSlotsSnapshot[slot.id] {
slot.dishId = snap.dishId
slot.isRuleOverridden = snap.isRuleOverridden
slot.isEatingOut = snap.isEatingOut
} else {
slot.dishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
}
}
@@ -323,7 +327,7 @@ final class HomeViewModel: ObservableObject {
guard !isApplyingUndo else { return }
lastWeekStartSnapshot = plan.weekStartDate
lastSlotsSnapshot = Dictionary(uniqueKeysWithValues: plan.slots.map { slot in
(slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden))
(slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut))
})
hasUndoSnapshot = true
}
@@ -336,7 +340,7 @@ final class HomeViewModel: ObservableObject {
private func firstFreeSlot(in plan: WeekPlan) -> MealSlot? {
plan.slots
.filter { $0.dishId == nil }
.filter { $0.dishId == nil && !$0.isEatingOut }
.sorted { lhs, rhs in
if lhs.dayOfWeek != rhs.dayOfWeek {
return lhs.dayOfWeek < rhs.dayOfWeek
@@ -355,7 +359,7 @@ final class HomeViewModel: ObservableObject {
switch settings.syncModeEnum {
case .weekComplete:
if plan.slots.allSatisfy({ $0.dishId != nil }) {
if plan.slots.allSatisfy({ $0.dishId != nil || $0.isEatingOut }) {
let descriptor = FetchDescriptor<Dish>()
let dishes = (try? plan.modelContext?.fetch(descriptor)) ?? []
syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings)
@@ -426,6 +430,33 @@ final class HomeViewModel: ObservableObject {
slot.isRuleOverridden = !valid
}
func markEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
captureUndoSnapshot(plan: plan)
if let eventId = slot.calendarEventId {
CalendarService.shared.deleteEvent(eventId: eventId)
slot.calendarEventId = nil
}
slot.dishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = true
plan.updatedAt = Date()
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
HapticManager.shared.impact(style: .medium)
showToastMessage(localizedString("toast_eating_out_marked", language: settings.languageEnum.resolved()))
}
func unmarkEatingOut(slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
captureUndoSnapshot(plan: plan)
slot.isEatingOut = false
plan.updatedAt = Date()
HapticManager.shared.impact(style: .light)
}
func acknowledgeViolation(slot: MealSlot, plan: WeekPlan) {
slot.isRuleOverridden = false
plan.updatedAt = Date()
}
func confirmInvalidDrop(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
invalidDropContext = InvalidDropContext(dish: dish, slot: slot)
HapticManager.shared.notification(type: .warning)