1.0.4: Smarter auto-assign, quick dish entry, calendar fix, frictionless onboarding
- AutocompleteEngine: no-repeat rule now applies to all dishes (not just tagless ones); fallback still allows repeats when no alternative exists - CalendarService: update existing events instead of delete+create to prevent duplicates; fix weekStartDate using plan date instead of currentWeekStart - DishFormView: form stays open after saving in create mode for quick multi-dish entry; Done button to close; saved feedback banner - OnboardingViewModel/FirstDishesStepView: dishes are now optional during onboarding; Finish button always enabled - AnalyticsService: new events — onboarding_step_viewed(step) and auto_assign_used(filled, unfilled) - Localization: dish_done and first_dishes_optional_hint added in 6 languages; release notes updated in all 6 languages - Version bump: 1.0.3 → 1.0.4 (build 19) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ final class DishViewModel: ObservableObject {
|
||||
tagIds: Array(selectedTagIds)
|
||||
)
|
||||
context.insert(dish)
|
||||
AnalyticsService.logDishAdded(tagCount: selectedTagIds.count)
|
||||
}
|
||||
|
||||
try? context.save()
|
||||
@@ -63,6 +64,7 @@ final class DishViewModel: ObservableObject {
|
||||
|
||||
context.delete(dish)
|
||||
try? context.save()
|
||||
AnalyticsService.logDishDeleted()
|
||||
reset()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ final class HomeViewModel: ObservableObject {
|
||||
plan.updatedAt = Date()
|
||||
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings)
|
||||
AnalyticsService.logMealAssigned()
|
||||
|
||||
HapticManager.shared.notification(type: .success)
|
||||
showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
|
||||
@@ -107,6 +108,7 @@ final class HomeViewModel: ObservableObject {
|
||||
slot.isRuleOverridden = false
|
||||
plan.updatedAt = Date()
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
|
||||
AnalyticsService.logMealCleared()
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
}
|
||||
|
||||
@@ -124,6 +126,7 @@ final class HomeViewModel: ObservableObject {
|
||||
|
||||
plan.updatedAt = Date()
|
||||
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
|
||||
AnalyticsService.logAutoAssignUsed(filledSlots: result.filledSlots.count, unfilledSlots: result.unfilledCount)
|
||||
|
||||
Task {
|
||||
for _ in result.filledSlots {
|
||||
@@ -159,6 +162,7 @@ final class HomeViewModel: ObservableObject {
|
||||
slot.isRuleOverridden = false
|
||||
}
|
||||
plan.updatedAt = Date()
|
||||
AnalyticsService.logWeekReset()
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
showToastMessage(localizedString("toast_week_reset", language: settings.languageEnum.resolved()))
|
||||
}
|
||||
@@ -378,19 +382,32 @@ final class HomeViewModel: ObservableObject {
|
||||
private func syncAllAssignedSlotsToCalendar(plan: WeekPlan, dishes: [Dish], settings: AppSettings) {
|
||||
let dishById = Dictionary(uniqueKeysWithValues: dishes.map { ($0.id, $0) })
|
||||
for slot in plan.slots {
|
||||
if let oldEventId = slot.calendarEventId {
|
||||
CalendarService.shared.deleteEvent(eventId: oldEventId)
|
||||
slot.calendarEventId = nil
|
||||
guard let dishId = slot.dishId, let dish = dishById[dishId] else {
|
||||
if let eventId = slot.calendarEventId {
|
||||
CalendarService.shared.deleteEvent(eventId: eventId)
|
||||
slot.calendarEventId = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
guard let dishId = slot.dishId, let dish = dishById[dishId] else { continue }
|
||||
slot.calendarEventId = CalendarService.shared.createEvent(
|
||||
slot: slot,
|
||||
dishName: dish.name,
|
||||
dishDescription: dish.descriptionText,
|
||||
weekStartDate: currentWeekStart,
|
||||
settings: settings
|
||||
)
|
||||
if let existingEventId = slot.calendarEventId {
|
||||
slot.calendarEventId = CalendarService.shared.updateEvent(
|
||||
eventId: existingEventId,
|
||||
slot: slot,
|
||||
dishName: dish.name,
|
||||
dishDescription: dish.descriptionText,
|
||||
weekStartDate: plan.weekStartDate,
|
||||
settings: settings
|
||||
)
|
||||
} else {
|
||||
slot.calendarEventId = CalendarService.shared.createEvent(
|
||||
slot: slot,
|
||||
dishName: dish.name,
|
||||
dishDescription: dish.descriptionText,
|
||||
weekStartDate: plan.weekStartDate,
|
||||
settings: settings
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
case 1: return true // Meal windows (always has selection)
|
||||
case 2: return true // Weekends (toggle)
|
||||
case 3: return true // Calendar (optional)
|
||||
case 4: return addedDishes.count >= 2 // Need at least 2 dishes
|
||||
case 4: return true // Dishes are optional
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,7 @@ final class OnboardingViewModel: ObservableObject {
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
AnalyticsService.logOnboardingCompleted()
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingAutoAssignPromptKey)
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user