1.2.1: surface "Copy previous week" on the empty-week banner

The exact-copy-from-previous-week action already existed but was buried in the
magic-wand long-press context menu. Expose it as a visible secondary button on
the auto-assign banner (shown only when the previous week actually has a menu),
so users landing on a fresh week can one-tap copy last week's plan and then tweak
it. Confirms before overwriting a week that already has dishes.

- Extract requestCopyPreviousWeek() shared by the menu and the banner.
- Add week_copied_previous analytics (copied_slots + source: empty_banner|menu).
- Reuses the existing localized home_copy_previous_week string (all 6 languages).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
This commit is contained in:
alexandrev-tibco
2026-07-11 10:58:53 +02:00
parent c5fd1088cd
commit 6ba71e6341
3 changed files with 78 additions and 31 deletions
+8
View File
@@ -21,6 +21,7 @@ enum AnalyticsEvent {
// Planner // Planner
static let autoAssignUsed = "auto_assign_used" static let autoAssignUsed = "auto_assign_used"
static let weekCopiedPrevious = "week_copied_previous"
// Onboarding // Onboarding
static let onboardingStepViewed = "onboarding_step_viewed" static let onboardingStepViewed = "onboarding_step_viewed"
@@ -112,6 +113,13 @@ enum AnalyticsService {
]) ])
} }
static func logWeekCopiedPrevious(copiedSlots: Int, source: String) {
logEvent(AnalyticsEvent.weekCopiedPrevious, parameters: [
"copied_slots": copiedSlots,
"source": source
])
}
static func logICloudSyncToggled(enabled: Bool) { static func logICloudSyncToggled(enabled: Bool) {
logEvent(AnalyticsEvent.iCloudSyncToggled, parameters: ["enabled": enabled]) logEvent(AnalyticsEvent.iCloudSyncToggled, parameters: ["enabled": enabled])
} }
+5 -1
View File
@@ -266,7 +266,8 @@ final class HomeViewModel: ObservableObject {
previousPlan: WeekPlan?, previousPlan: WeekPlan?,
settings: AppSettings, settings: AppSettings,
allTags: [Tag], allTags: [Tag],
allDishes: [Dish] allDishes: [Dish],
source: String
) { ) {
guard let previousPlan else { guard let previousPlan else {
showToastMessage(localizedString("toast_previous_week_empty", language: settings.languageEnum.resolved())) showToastMessage(localizedString("toast_previous_week_empty", language: settings.languageEnum.resolved()))
@@ -282,6 +283,7 @@ final class HomeViewModel: ObservableObject {
} }
let dishIds = Set(allDishes.map(\.id)) let dishIds = Set(allDishes.map(\.id))
var copiedCount = 0
for slot in currentPlan.slots { for slot in currentPlan.slots {
if let eventId = slot.calendarEventId { if let eventId = slot.calendarEventId {
CalendarService.shared.deleteEvent(eventId: eventId) CalendarService.shared.deleteEvent(eventId: eventId)
@@ -291,6 +293,7 @@ final class HomeViewModel: ObservableObject {
let key = SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType) let key = SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType)
if let previousDishId = previousByKey[key]?.dishId, dishIds.contains(previousDishId) { if let previousDishId = previousByKey[key]?.dishId, dishIds.contains(previousDishId) {
slot.dishId = previousDishId slot.dishId = previousDishId
copiedCount += 1
} else { } else {
slot.dishId = nil slot.dishId = nil
} }
@@ -301,6 +304,7 @@ final class HomeViewModel: ObservableObject {
currentPlan.updatedAt = Date() currentPlan.updatedAt = Date()
applyCalendarSyncPolicy(plan: currentPlan, settings: settings) applyCalendarSyncPolicy(plan: currentPlan, settings: settings)
HapticManager.shared.notification(type: .success) HapticManager.shared.notification(type: .success)
AnalyticsService.logWeekCopiedPrevious(copiedSlots: copiedCount, source: source)
showToastMessage(localizedString("toast_copied_previous_week", language: settings.languageEnum.resolved())) showToastMessage(localizedString("toast_copied_previous_week", language: settings.languageEnum.resolved()))
} }
+65 -30
View File
@@ -23,6 +23,7 @@ struct HomeView: View {
@State private var showWeekPicker: Bool = false @State private var showWeekPicker: Bool = false
@State private var weekPickerDate: Date = Date() @State private var weekPickerDate: Date = Date()
@State private var showCopyPreviousConfirm: Bool = false @State private var showCopyPreviousConfirm: Bool = false
@State private var copyPreviousSource: String = "menu"
@State private var showReviewSentimentPrompt: Bool = false @State private var showReviewSentimentPrompt: Bool = false
@State private var showReviewSupportPrompt: Bool = false @State private var showReviewSupportPrompt: Bool = false
@State private var showPostOnboardingAutoAssignPrompt: Bool = false @State private var showPostOnboardingAutoAssignPrompt: Bool = false
@@ -149,11 +150,7 @@ struct HomeView: View {
.disabled(!viewModel.canUndo(for: plan)) .disabled(!viewModel.canUndo(for: plan))
Button { Button {
if plan.slots.contains(where: { $0.dishId != nil }) { requestCopyPreviousWeek(plan: plan, settings: settings, source: "menu")
showCopyPreviousConfirm = true
} else {
copyFromPreviousWeek(currentPlan: plan, settings: settings)
}
} label: { } label: {
Label("home_copy_previous_week", systemImage: "doc.on.doc") Label("home_copy_previous_week", systemImage: "doc.on.doc")
} }
@@ -774,32 +771,53 @@ struct HomeView: View {
@ViewBuilder @ViewBuilder
private func autoAssignBanner(emptyCount: Int, plan: WeekPlan, settings: AppSettings) -> some View { private func autoAssignBanner(emptyCount: Int, plan: WeekPlan, settings: AppSettings) -> some View {
HStack(spacing: 12) { VStack(spacing: 10) {
VStack(alignment: .leading, spacing: 2) { HStack(spacing: 12) {
Text("home_auto_assign_cta_title") VStack(alignment: .leading, spacing: 2) {
.font(.mealMoodBodyBold) Text("home_auto_assign_cta_title")
.foregroundColor(.mealMoodTextPrimary) .font(.mealMoodBodyBold)
Text(String(format: String(localized: "home_auto_assign_cta_slots"), emptyCount)) .foregroundColor(.mealMoodTextPrimary)
.font(.mealMoodCaption) Text(String(format: String(localized: "home_auto_assign_cta_slots"), emptyCount))
.foregroundColor(.mealMoodTextSecondary) .font(.mealMoodCaption)
} .foregroundColor(.mealMoodTextSecondary)
Spacer()
Button {
viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings, allPlans: weekPlans)
} label: {
HStack(spacing: 6) {
Image(systemName: "wand.and.stars")
Text("home_auto_assign_cta_button")
.font(.mealMoodSmall.weight(.semibold))
} }
.foregroundColor(.white) Spacer()
.padding(.horizontal, 14) Button {
.padding(.vertical, 8) viewModel.autoComplete(plan: plan, dishes: dishes, tags: tags, settings: settings, allPlans: weekPlans)
.background(Color.mealMoodCoral) } label: {
.clipShape(Capsule()) 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)
}
if previousWeekHasMenu() {
Button {
requestCopyPreviousWeek(plan: plan, settings: settings, source: "empty_banner")
} label: {
HStack(spacing: 6) {
Image(systemName: "doc.on.doc")
Text("home_copy_previous_week")
.font(.mealMoodSmall.weight(.semibold))
}
.foregroundColor(.mealMoodCoral)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(Capsule().fill(Color.white.opacity(0.75)))
.overlay(Capsule().stroke(Color.mealMoodCoral.opacity(0.4), lineWidth: 1))
}
.buttonStyle(.plain)
.disabled(viewModel.isAutoCompleting)
} }
.buttonStyle(.plain)
.disabled(viewModel.isAutoCompleting)
} }
.padding(12) .padding(12)
.background( .background(
@@ -869,6 +887,22 @@ struct HomeView: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }
private func previousWeekHasMenu() -> Bool {
guard let previous = fetchWeekPlan(for: viewModel.currentWeekStart.addingDays(-7)) else { return false }
return previous.slots.contains { $0.dishId != nil }
}
/// Copies the previous week, asking to confirm first only when the current
/// week already has dishes that would be overwritten.
private func requestCopyPreviousWeek(plan: WeekPlan, settings: AppSettings, source: String) {
copyPreviousSource = source
if plan.slots.contains(where: { $0.dishId != nil }) {
showCopyPreviousConfirm = true
} else {
copyFromPreviousWeek(currentPlan: plan, settings: settings)
}
}
private func copyFromPreviousWeek(currentPlan: WeekPlan, settings: AppSettings) { private func copyFromPreviousWeek(currentPlan: WeekPlan, settings: AppSettings) {
let previousPlan = fetchWeekPlan(for: viewModel.currentWeekStart.addingDays(-7)) let previousPlan = fetchWeekPlan(for: viewModel.currentWeekStart.addingDays(-7))
viewModel.copyFromPreviousWeek( viewModel.copyFromPreviousWeek(
@@ -876,7 +910,8 @@ struct HomeView: View {
previousPlan: previousPlan, previousPlan: previousPlan,
settings: settings, settings: settings,
allTags: tags, allTags: tags,
allDishes: dishes allDishes: dishes,
source: copyPreviousSource
) )
} }