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:
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user