platos: segundo plato por comida (ensalada + salmon)

- MealSlot.secondaryDishId (opcional, aditivo CloudKit); se limpia al
  sustituir/quitar/vaciar/marcar comer fuera y viaja en swaps, copia de
  semana anterior, undo y snapshot KV de iCloud (retrocompatible)
- Picker de hueco lleno: toggle "anadir como segundo plato" + boton para
  quitarlo; HomeViewModel.assignSecondaryDish/removeSecondaryDish
- Se muestra "Plato + Segundo" en calendario, widget y export; la lista
  de la compra incluye los ingredientes del segundo; stats lo cuentan
- Evento analytics secondary_dish_added; strings en 6 idiomas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
This commit is contained in:
alexandrev-tibco
2026-09-01 11:37:23 +02:00
parent 7193234024
commit a0f9fa63d3
15 changed files with 128 additions and 13 deletions
+33 -1
View File
@@ -11,6 +11,7 @@ final class HomeViewModel: ObservableObject {
private struct SlotSnapshot {
let dishId: UUID?
let secondaryDishId: UUID?
let isRuleOverridden: Bool
let isEatingOut: Bool
}
@@ -94,6 +95,7 @@ final class HomeViewModel: ObservableObject {
}
slot.dishId = dish.id
slot.secondaryDishId = nil
slot.isRuleOverridden = isOverride
plan.updatedAt = Date()
@@ -104,6 +106,25 @@ final class HomeViewModel: ObservableObject {
showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
}
/// Adds a companion dish to an already-filled slot ("ensalada + salmón").
func assignSecondaryDish(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
guard slot.dishId != nil, slot.dishId != dish.id else { return }
captureUndoSnapshot(plan: plan)
slot.secondaryDishId = dish.id
plan.updatedAt = Date()
AnalyticsService.logEvent("secondary_dish_added")
HapticManager.shared.notification(type: .success)
showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
}
func removeSecondaryDish(from slot: MealSlot, plan: WeekPlan) {
guard slot.secondaryDishId != nil else { return }
captureUndoSnapshot(plan: plan)
slot.secondaryDishId = nil
plan.updatedAt = Date()
HapticManager.shared.notification(type: .warning)
}
func removeDish(from slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
captureUndoSnapshot(plan: plan)
@@ -112,6 +133,7 @@ final class HomeViewModel: ObservableObject {
slot.calendarEventId = nil
}
slot.dishId = nil
slot.secondaryDishId = nil
slot.isRuleOverridden = false
plan.updatedAt = Date()
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
@@ -182,6 +204,7 @@ final class HomeViewModel: ObservableObject {
slot.calendarEventId = nil
}
slot.dishId = nil
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
}
@@ -247,8 +270,11 @@ final class HomeViewModel: ObservableObject {
}
let targetDishId = targetSlot.dishId
let targetSecondaryId = targetSlot.secondaryDishId
sourceSlot.dishId = targetDishId
targetSlot.dishId = sourceDishId
targetSlot.secondaryDishId = sourceSlot.secondaryDishId
sourceSlot.secondaryDishId = targetSecondaryId
sourceSlot.isRuleOverridden = false
targetSlot.isRuleOverridden = false
@@ -293,9 +319,12 @@ final class HomeViewModel: ObservableObject {
let key = SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType)
if let previousDishId = previousByKey[key]?.dishId, dishIds.contains(previousDishId) {
slot.dishId = previousDishId
let previousSecondary = previousByKey[key]?.secondaryDishId
slot.secondaryDishId = previousSecondary.flatMap { dishIds.contains($0) ? $0 : nil }
copiedCount += 1
} else {
slot.dishId = nil
slot.secondaryDishId = nil
}
slot.isRuleOverridden = false
updateRuleOverrideFlag(for: slot, plan: currentPlan, allTags: allTags, allDishes: allDishes)
@@ -324,10 +353,12 @@ final class HomeViewModel: ObservableObject {
if let snap = lastSlotsSnapshot[slot.id] {
slot.dishId = snap.dishId
slot.secondaryDishId = snap.secondaryDishId
slot.isRuleOverridden = snap.isRuleOverridden
slot.isEatingOut = snap.isEatingOut
} else {
slot.dishId = nil
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
}
@@ -353,7 +384,7 @@ final class HomeViewModel: ObservableObject {
guard !isApplyingUndo else { return }
lastWeekStartSnapshot = plan.weekStartDate
lastSlotsSnapshot = Dictionary(plan.slotList.map { slot in
(slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut))
(slot.id, SlotSnapshot(dishId: slot.dishId, secondaryDishId: slot.secondaryDishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut))
}, uniquingKeysWith: { first, _ in first })
hasUndoSnapshot = true
}
@@ -463,6 +494,7 @@ final class HomeViewModel: ObservableObject {
slot.calendarEventId = nil
}
slot.dishId = nil
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = true
plan.updatedAt = Date()