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:
alexandrev-tibco
2026-04-19 11:35:50 +02:00
parent c66132f494
commit 6c7e12b41f
25 changed files with 659 additions and 49 deletions
+2
View File
@@ -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
}
+28 -11
View File
@@ -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