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:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -49,11 +49,15 @@ struct ShoppingListView: View {
|
||||
}
|
||||
|
||||
private var freeTextItems: [ShoppingItem] {
|
||||
items.filter { $0.dishId == nil }
|
||||
items.filter { $0.dishId == nil && !$0.isDismissed }
|
||||
}
|
||||
|
||||
private var dismissedItems: [ShoppingItem] {
|
||||
items.filter(\.isDismissed)
|
||||
}
|
||||
|
||||
private func items(for dish: Dish) -> [ShoppingItem] {
|
||||
items.filter { $0.dishId == dish.id }
|
||||
items.filter { $0.dishId == dish.id && !$0.isDismissed }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -66,10 +70,24 @@ struct ShoppingListView: View {
|
||||
ForEach(plannedDishes, id: \.id) { dish in
|
||||
let dishItems = items(for: dish)
|
||||
if !dishItems.isEmpty {
|
||||
Section(dish.name) {
|
||||
Section {
|
||||
ForEach(dishItems, id: \.id) { item in
|
||||
itemRow(item)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(dish.name)
|
||||
Spacer()
|
||||
// "Already cooked": park every line of this dish
|
||||
Button {
|
||||
dismissDish(dish)
|
||||
} label: {
|
||||
Label("shopping_exclude_dish", systemImage: "takeoutbag.and.cup.and.straw")
|
||||
.font(.mealMoodCaption.weight(.semibold))
|
||||
.foregroundColor(.mealMoodCoral)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +100,28 @@ struct ShoppingListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if !dismissedItems.isEmpty {
|
||||
Section("shopping_at_home_section") {
|
||||
ForEach(dismissedItems, id: \.id) { item in
|
||||
Button {
|
||||
item.isDismissed = false
|
||||
HapticManager.shared.selection()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "house.fill")
|
||||
.foregroundColor(.mealMoodMint)
|
||||
Text(item.title)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
Spacer()
|
||||
Image(systemName: "arrow.uturn.backward.circle")
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("shopping_other_items") {
|
||||
ForEach(freeTextItems, id: \.id) { item in
|
||||
itemRow(item)
|
||||
@@ -187,6 +227,23 @@ struct ShoppingListView: View {
|
||||
Label("shopping_delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .leading) {
|
||||
Button {
|
||||
item.isDismissed = true
|
||||
HapticManager.shared.selection()
|
||||
} label: {
|
||||
Label("shopping_have_it", systemImage: "house")
|
||||
}
|
||||
.tint(.mealMoodMint)
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissDish(_ dish: Dish) {
|
||||
for item in items where item.dishId == dish.id {
|
||||
item.isDismissed = true
|
||||
}
|
||||
AnalyticsService.logEvent("shopping_dish_excluded")
|
||||
HapticManager.shared.notification(type: .success)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -274,7 +331,7 @@ struct ShoppingListView: View {
|
||||
|
||||
private var exportText: String {
|
||||
ShoppingListService.exportText(
|
||||
items: items,
|
||||
items: items.filter { !$0.isDismissed },
|
||||
header: String(localized: "shopping_share_header")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ struct StatsView: View {
|
||||
|
||||
private var completeWeeks: Int {
|
||||
weekPlans.filter { plan in
|
||||
!plan.slotList.isEmpty && plan.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut }
|
||||
!plan.slotList.isEmpty && plan.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut || $0.isSkipped }
|
||||
}.count
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user