1.0.6: Priority dishes, day rules, auto-assign CTA, tap-to-replace, export incomplete weeks, Sunday notification
- Dish.isPriority: mark dishes as priority; auto-assign gives them 2.5x weight - Tag.dayRestriction: restrict tags to weekdays or weekend only - Auto-assign CTA banner shown when there are empty slots (was hidden in context menu) - Tap on filled slot opens picker to replace dish directly (no need to remove first) - Export callout visible as soon as ≥1 slot is filled (not only when week is complete) - WeekPlanShareView shows localized "TBD" / "Eating out" for empty/eating-out slots - Second Sunday 17:00 push notification to plan next week - DishDrawer sorts: priority first, then by historical usage, then alphabetically - Search in SlotDishPickerSheet already shipped in 1.0.5 (verified ✓) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ struct HomeView: View {
|
||||
@State private var showWeekLimitUpsell: Bool = false
|
||||
@State private var wasWeekComplete: Bool = false
|
||||
@State private var selectedEmptySlotId: UUID?
|
||||
@State private var selectedFilledSlotId: UUID?
|
||||
@State private var showWeekPicker: Bool = false
|
||||
@State private var weekPickerDate: Date = Date()
|
||||
@State private var showCopyPreviousConfirm: Bool = false
|
||||
@@ -225,13 +226,22 @@ struct HomeView: View {
|
||||
viewModel: viewModel,
|
||||
onTapEmptySlot: { slot in
|
||||
selectedEmptySlotId = slot.id
|
||||
},
|
||||
onTapFilledSlot: { slot in
|
||||
selectedFilledSlotId = slot.id
|
||||
}
|
||||
)
|
||||
|
||||
if isWeekComplete(plan: plan) {
|
||||
let filledCount = plan.slots.filter { $0.dishId != nil || $0.isEatingOut }.count
|
||||
if filledCount > 0 {
|
||||
exportCallout(plan: plan, settings: settings)
|
||||
}
|
||||
|
||||
let emptyCount = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }.count
|
||||
if viewModel.canEditCurrentWeek && !dishes.isEmpty && emptyCount > 0 {
|
||||
autoAssignBanner(emptyCount: emptyCount, plan: plan, settings: settings)
|
||||
}
|
||||
|
||||
ScrollView(showsIndicators: true) {
|
||||
dishDrawer(plan: plan, settings: settings)
|
||||
.padding(.bottom, 0)
|
||||
@@ -403,6 +413,36 @@ struct HomeView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(
|
||||
isPresented: Binding(
|
||||
get: { selectedFilledSlotId != nil },
|
||||
set: { isPresented in
|
||||
if !isPresented { selectedFilledSlotId = nil }
|
||||
}
|
||||
)
|
||||
) {
|
||||
if let slotId = selectedFilledSlotId,
|
||||
plan.slots.contains(where: { $0.id == slotId }) {
|
||||
SlotDishPickerSheet(
|
||||
dishes: dishes,
|
||||
tags: tags,
|
||||
onPickDish: { dish in
|
||||
guard let freshSlot = plan.slots.first(where: { $0.id == slotId }) else {
|
||||
selectedFilledSlotId = nil
|
||||
return
|
||||
}
|
||||
viewModel.assignDish(dish, to: freshSlot, plan: plan, settings: settings, isOverride: true)
|
||||
selectedFilledSlotId = nil
|
||||
},
|
||||
onCreateDish: {
|
||||
selectedFilledSlotId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
viewModel.showDishForm = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.sheet(item: $editingDish) { dish in
|
||||
DishFormView(dish: dish)
|
||||
}
|
||||
@@ -591,6 +631,7 @@ struct HomeView: View {
|
||||
tags: tags,
|
||||
language: settings.languageEnum.resolved(),
|
||||
usedDishIds: Set(plan.slots.compactMap(\.dishId)),
|
||||
usageRanking: dishUsageCounts,
|
||||
onAddDish: { viewModel.showDishForm = true },
|
||||
onQuickAssignDish: { dish in
|
||||
viewModel.assignDishToFirstFreeSlot(
|
||||
@@ -651,6 +692,59 @@ struct HomeView: View {
|
||||
plan.slots.allSatisfy { $0.dishId != nil || $0.isEatingOut }
|
||||
}
|
||||
|
||||
private var dishUsageCounts: [UUID: Int] {
|
||||
var counts: [UUID: Int] = [:]
|
||||
for plan in weekPlans {
|
||||
for slot in plan.slots {
|
||||
if let dishId = slot.dishId {
|
||||
counts[dishId, default: 0] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func autoAssignBanner(emptyCount: Int, plan: WeekPlan, settings: AppSettings) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("home_auto_assign_cta_title")
|
||||
.font(.mealMoodBodyBold)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
Text(String(format: String(localized: "home_auto_assign_cta_slots"), emptyCount))
|
||||
.font(.mealMoodCaption)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings)
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "wand.and.stars")
|
||||
Text("home_auto_assign_cta_button")
|
||||
.font(.mealMoodSmall.weight(.semibold))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color.mealMoodCoral)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(viewModel.isAutoCompleting)
|
||||
}
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.mealMoodCoral.opacity(0.08))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Color.mealMoodCoral.opacity(0.25), lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
private func updateWidget(settings: AppSettings) {
|
||||
let todayPlan = fetchWeekPlan(for: Date().startOfWeek())
|
||||
WidgetDataStore.update(plan: todayPlan, dishes: dishes, settings: settings)
|
||||
|
||||
Reference in New Issue
Block a user