huecos sin planificar y exclusiones en la lista de la compra

- MealSlot.isSkipped: opcion "No planificar esta comida" en el picker de
  hueco vacio — cuenta como resuelto (semana completa, notificaciones,
  stats) y el autocompletado lo respeta; vista gris con guion, tap para
  desmarcar; viaja en undo, snapshot KV (junto con isEatingOut, que
  faltaba en el payload) y exports (— en imagen, omitido en texto)
- ShoppingItem.isDismissed: "Ya lo tengo" por ingrediente (swipe
  izquierdo) y "Ya esta hecho" por plato entero (boton en la cabecera de
  su seccion); van a la seccion "Ya en casa" con restauracion de un tap,
  y quedan fuera del export de texto. No se borran porque reconcile los
  recrearia
- Esquema CloudKit Development: CD_isSkipped, CD_isDismissed y el record
  type CD_ShoppingItem entero, que no existia — la lista de la compra no
  estaba sincronizando entre dispositivos (mismo bug que photoData)

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 16:16:34 +02:00
parent ed1a0f566f
commit 6fe16d8e29
19 changed files with 227 additions and 24 deletions
+36 -4
View File
@@ -14,6 +14,7 @@ final class HomeViewModel: ObservableObject {
let secondaryDishId: UUID?
let isRuleOverridden: Bool
let isEatingOut: Bool
let isSkipped: Bool
}
@Published var currentWeekStart: Date
@@ -150,7 +151,7 @@ final class HomeViewModel: ObservableObject {
.sorted { $0.weekStartDate > $1.weekStartDate }
.prefix(12)
let emptySlots = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut }
let emptySlots = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut && !$0.isSkipped }
let result = AutocompleteEngine.autocomplete(
emptySlots: emptySlots,
currentPlan: plan,
@@ -207,6 +208,7 @@ final class HomeViewModel: ObservableObject {
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
slot.isSkipped = false
}
plan.updatedAt = Date()
AnalyticsService.logWeekReset()
@@ -356,11 +358,13 @@ final class HomeViewModel: ObservableObject {
slot.secondaryDishId = snap.secondaryDishId
slot.isRuleOverridden = snap.isRuleOverridden
slot.isEatingOut = snap.isEatingOut
slot.isSkipped = snap.isSkipped
} else {
slot.dishId = nil
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
slot.isSkipped = false
}
}
@@ -384,7 +388,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, secondaryDishId: slot.secondaryDishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut))
(slot.id, SlotSnapshot(dishId: slot.dishId, secondaryDishId: slot.secondaryDishId, isRuleOverridden: slot.isRuleOverridden, isEatingOut: slot.isEatingOut, isSkipped: slot.isSkipped))
}, uniquingKeysWith: { first, _ in first })
hasUndoSnapshot = true
}
@@ -397,7 +401,7 @@ final class HomeViewModel: ObservableObject {
private func firstFreeSlot(in plan: WeekPlan) -> MealSlot? {
plan.slotList
.filter { $0.dishId == nil && !$0.isEatingOut }
.filter { $0.dishId == nil && !$0.isEatingOut && !$0.isSkipped }
.sorted { lhs, rhs in
if lhs.dayOfWeek != rhs.dayOfWeek {
return lhs.dayOfWeek < rhs.dayOfWeek
@@ -416,7 +420,7 @@ final class HomeViewModel: ObservableObject {
switch settings.syncModeEnum {
case .weekComplete:
if plan.slotList.allSatisfy({ $0.dishId != nil || $0.isEatingOut }) {
if plan.slotList.allSatisfy({ $0.dishId != nil || $0.isEatingOut || $0.isSkipped }) {
let descriptor = FetchDescriptor<Dish>()
let dishes = (try? plan.modelContext?.fetch(descriptor)) ?? []
syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings)
@@ -497,6 +501,7 @@ final class HomeViewModel: ObservableObject {
slot.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = true
slot.isSkipped = false
plan.updatedAt = Date()
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
HapticManager.shared.impact(style: .medium)
@@ -510,6 +515,33 @@ final class HomeViewModel: ObservableObject {
HapticManager.shared.impact(style: .light)
}
/// "Don't plan this meal": resolves the slot without a dish, so the week
/// can complete and autocomplete leaves it alone.
func markSkipped(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.secondaryDishId = nil
slot.isRuleOverridden = false
slot.isEatingOut = false
slot.isSkipped = true
plan.updatedAt = Date()
applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
AnalyticsService.logEvent("meal_skipped")
HapticManager.shared.impact(style: .medium)
showToastMessage(localizedString("toast_meal_skipped", language: settings.languageEnum.resolved()))
}
func unmarkSkipped(slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
captureUndoSnapshot(plan: plan)
slot.isSkipped = false
plan.updatedAt = Date()
HapticManager.shared.impact(style: .light)
}
func acknowledgeViolation(slot: MealSlot, plan: WeekPlan) {
slot.isRuleOverridden = false
plan.updatedAt = Date()