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:
alexandrev-tibco
2026-04-30 00:00:53 +02:00
parent 92f97f855d
commit a4dfb38feb
22 changed files with 344 additions and 26 deletions
+95 -1
View File
@@ -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)