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
+26 -5
View File
@@ -202,7 +202,7 @@ struct HomeView: View {
HStack(spacing: 10) {
if let settings = settings,
let plan = fetchWeekPlan(for: viewModel.currentWeekStart) {
if plan.slotList.contains(where: { $0.dishId != nil || $0.isEatingOut }) {
if plan.slotList.contains(where: { $0.dishId != nil || $0.isEatingOut || $0.isSkipped }) {
Button {
requestWeekShare(plan: plan, settings: settings)
} label: {
@@ -337,8 +337,8 @@ struct HomeView: View {
calendarResizeHandle(availableHeight: contentGeo.size.height)
}
let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut }.count
let emptyCount = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut }.count
let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut || $0.isSkipped }.count
let emptyCount = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut && !$0.isSkipped }.count
// Only surface the export banner once the week is actually
// complete (no empty slots) and let the user dismiss it.
if filledCount > 0 && emptyCount == 0 && !isExportCalloutDismissed(plan) {
@@ -537,6 +537,14 @@ struct HomeView: View {
}
selectedEmptySlotId = nil
viewModel.markEatingOut(slot: freshSlot, plan: plan, settings: settings)
},
onSkip: {
guard let freshSlot = plan.slotList.first(where: { $0.id == slotId }) else {
selectedEmptySlotId = nil
return
}
selectedEmptySlotId = nil
viewModel.markSkipped(slot: freshSlot, plan: plan, settings: settings)
}
)
}
@@ -669,6 +677,7 @@ struct HomeView: View {
let onPickDish: (Dish) -> Void
let onCreateDish: () -> Void
var onEatingOut: (() -> Void)? = nil
var onSkip: (() -> Void)? = nil
/// Set for filled slots: taps add a companion dish instead of replacing
/// when the "second dish" mode is on.
var onPickSecondDish: ((Dish) -> Void)? = nil
@@ -721,6 +730,18 @@ struct HomeView: View {
.listRowBackground(Color(hex: "#EEF8F5"))
}
if let onSkip, searchText.isEmpty {
Button {
dismiss()
onSkip()
} label: {
Label("home_mark_skipped", systemImage: "minus.circle")
.font(.mealMoodBody)
.foregroundColor(Color(hex: "#8E8B86"))
}
.listRowBackground(Color(hex: "#F5F3F0"))
}
if onPickSecondDish != nil, searchText.isEmpty {
Toggle(isOn: $asSecondDish) {
Label("home_pick_second_dish", systemImage: "plus.square.on.square")
@@ -898,7 +919,7 @@ struct HomeView: View {
}
private func isWeekComplete(plan: WeekPlan) -> Bool {
plan.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut }
plan.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut || $0.isSkipped }
}
private var dishUsageCounts: [UUID: Int] {
@@ -1214,7 +1235,7 @@ struct HomeView: View {
// instead of being asked. Reuses the home's autoComplete choreography.
if UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingAutoFillOnLaunchKey) {
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingAutoFillOnLaunchKey)
if !dishes.isEmpty && plan.slotList.contains(where: { $0.dishId == nil && !$0.isEatingOut }) {
if !dishes.isEmpty && plan.slotList.contains(where: { $0.dishId == nil && !$0.isEatingOut && !$0.isSkipped }) {
viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings, allPlans: weekPlans)
}
schedulePostOnboardingPremiumPromptIfNeeded(settings: settings)
+13 -2
View File
@@ -216,7 +216,8 @@ struct WeekCalendarView: View {
dishId: preferred.dishId,
secondaryDishId: preferred.secondaryDishId,
isRuleOverridden: preferred.isRuleOverridden,
isEatingOut: preferred.isEatingOut
isEatingOut: preferred.isEatingOut,
isSkipped: preferred.isSkipped
)
}
@@ -285,6 +286,14 @@ struct WeekCalendarView: View {
onTapFilledSlot?(live)
}
.draggable("slot:\(slot.slotId.uuidString)")
} else if let slot = slot, slot.isSkipped {
SkippedSlotView(mealType: mealType)
.contentShape(Rectangle())
.onTapGesture {
guard viewModel.canEditCurrentWeek else { return }
guard let live = liveSlot(for: slot) else { return }
viewModel.unmarkSkipped(slot: live, plan: plan, settings: settings)
}
} else if let slot = slot, slot.isEatingOut {
EatingOutSlotView(mealType: mealType)
.contentShape(Rectangle())
@@ -326,6 +335,7 @@ struct WeekCalendarView: View {
let secondaryDishId: UUID?
let isRuleOverridden: Bool
let isEatingOut: Bool
let isSkipped: Bool
}
private var dishesSnapshotKey: String {
@@ -375,7 +385,8 @@ struct WeekCalendarView: View {
dishId: preferred.dishId,
secondaryDishId: preferred.secondaryDishId,
isRuleOverridden: preferred.isRuleOverridden,
isEatingOut: preferred.isEatingOut
isEatingOut: preferred.isEatingOut,
isSkipped: preferred.isSkipped
)
}
@@ -793,6 +793,7 @@ struct WeekPlanShareView: View {
let slot = plan.slotList.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue }
guard let slot else { return String(localized: "share_slot_empty") }
if slot.isEatingOut { return String(localized: "slot_eating_out") }
if slot.isSkipped { return "" }
guard let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else {
return String(localized: "share_slot_empty")
}