import SwiftUI struct GoalsView: View { @EnvironmentObject private var accountStore: AccountStore @StateObject private var viewModel = GoalsViewModel() @State private var showingAddGoal = false @State private var editingGoal: Goal? @State private var goalFilter: GoalFilter = .active @State private var goalToDelete: Goal? private enum GoalFilter: String, CaseIterable, Identifiable { case active, archived, all var id: String { rawValue } var label: String { switch self { case .active: return String(localized: "goals_filter_active") case .archived: return String(localized: "goals_filter_archived") case .all: return String(localized: "goals_filter_all") } } } var body: some View { NavigationStack { ZStack { AppBackground() List { if filteredGoals.isEmpty { emptyState(for: goalFilter) } else { Section { ForEach(filteredGoals) { goal in GoalRowView( goal: goal, progress: viewModel.progress(for: goal), totalValue: viewModel.totalValue(for: goal), paceStatus: viewModel.paceStatus(for: goal), estimatedCompletionDate: viewModel.estimateCompletionDate(for: goal), onEdit: { editingGoal = goal } ) .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { goalToDelete = goal } label: { Label("Delete", systemImage: "trash") } } .swipeActions(edge: .leading, allowsFullSwipe: false) { Button { viewModel.archiveGoal(goal) } label: { Label( goal.isActive ? String(localized: "goal_archive") : String(localized: "goal_unarchive"), systemImage: goal.isActive ? "archivebox" : "arrow.uturn.backward" ) } .tint(goal.isActive ? .orange : .blue) Button { editingGoal = goal } label: { Label("Edit", systemImage: "pencil") } .tint(.appSecondary) } } } } } .scrollContentBackground(.hidden) } .navigationTitle("Goals") .toolbar { ToolbarItem(placement: .navigationBarLeading) { Picker(String(localized: "goals_filter_active"), selection: $goalFilter) { ForEach(GoalFilter.allCases) { filter in Text(filter.label).tag(filter) } } .pickerStyle(.menu) } ToolbarItem(placement: .navigationBarTrailing) { Button { showingAddGoal = true } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showingAddGoal) { GoalEditorView(account: accountStore.showAllAccounts ? nil : accountStore.selectedAccount) } .sheet(item: $editingGoal) { goal in GoalEditorView(account: goal.account, goal: goal) } .onAppear { viewModel.selectedAccount = accountStore.selectedAccount viewModel.showAllAccounts = accountStore.showAllAccounts viewModel.refresh() } .onReceive(accountStore.$selectedAccount) { account in viewModel.selectedAccount = account viewModel.refresh() } .onReceive(accountStore.$showAllAccounts) { showAll in viewModel.showAllAccounts = showAll viewModel.refresh() } .alert(String(localized: "goal_delete_title"), isPresented: Binding( get: { goalToDelete != nil }, set: { if !$0 { goalToDelete = nil } } )) { Button(String(localized: "goal_delete_confirm"), role: .destructive) { if let goal = goalToDelete { viewModel.deleteGoal(goal) } goalToDelete = nil } Button(String(localized: "cancel"), role: .cancel) { goalToDelete = nil } } message: { Text(String(localized: "goal_delete_message")) } } } private var filteredGoals: [Goal] { switch goalFilter { case .active: return viewModel.goals.filter { $0.isActive } case .archived: return viewModel.goals.filter { !$0.isActive } case .all: return viewModel.goals } } @ViewBuilder private func emptyState(for filter: GoalFilter) -> some View { switch filter { case .active: if viewModel.goals.isEmpty { VStack(spacing: 16) { Image(systemName: "target") .font(.system(size: 48)) .foregroundColor(.secondary) Text("Set your first goal") .font(.headline) Text("Track progress toward milestones like \(AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currencySymbol)1M and share your wins.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) Button { showingAddGoal = true } label: { Text(String(localized: "goals_empty_add_cta")) .font(.subheadline.weight(.semibold)) .foregroundColor(.white) .padding(.horizontal, 20) .padding(.vertical, 10) .background(Color.appPrimary) .clipShape(Capsule()) } } .frame(maxWidth: .infinity) .padding(.vertical, 32) } else { VStack(spacing: 12) { Image(systemName: "archivebox") .font(.system(size: 40)) .foregroundColor(.secondary) Text(String(localized: "goals_all_active_achieved")) .font(.headline) .foregroundColor(.secondary) Text("Switch to \"Archived\" or \"All\" to see other goals.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 32) } case .archived: VStack(spacing: 12) { Image(systemName: "archivebox") .font(.system(size: 40)) .foregroundColor(.secondary) Text(String(localized: "goals_empty_archived")) .font(.headline) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .padding(.vertical, 32) case .all: VStack(spacing: 16) { Image(systemName: "target") .font(.system(size: 48)) .foregroundColor(.secondary) Text("Set your first goal") .font(.headline) Text("Track progress toward milestones like \(AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currencySymbol)1M and share your wins.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 32) } } } struct GoalRowView: View { let goal: Goal let progress: Double let totalValue: Decimal let paceStatus: GoalPaceStatus? let estimatedCompletionDate: Date? let onEdit: () -> Void @State private var showingShareOptions = false private var isAchieved: Bool { GoalsViewModel.isAchieved(progress: progress) } private var targetUrgency: GoalUrgencyLevel { GoalsViewModel.urgencyLevel( targetDate: goal.targetDate, isBehind: paceStatus?.isBehind ?? false, isAchieved: isAchieved ) } private var targetDateColor: Color { switch targetUrgency { case .normal: return .secondary case .warning: return .appWarning case .critical: return .negativeRed } } var body: some View { ZStack(alignment: .topTrailing) { Button(action: onEdit) { VStack(alignment: .leading, spacing: 12) { HStack { Text(goal.name) .font(.headline) .foregroundColor(isAchieved ? .appSecondary : .primary) if isAchieved { Text("Achieved") .font(.caption2.weight(.bold)) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.appSecondary) .clipShape(Capsule()) } } GoalProgressBar( progress: progress, tint: isAchieved ? .appSuccess : .appSecondary, iconColor: isAchieved ? .appSuccess : .appSecondary ) HStack { Text(totalValue.currencyString) .font(.subheadline.weight(.semibold)) Spacer() Text("of \(goal.targetDecimal.currencyString)") .font(.subheadline) .foregroundColor(.secondary) } if let targetDate = goal.targetDate { Text("Target date: \(targetDate.mediumDateString)") .font(.caption) .foregroundColor(targetDateColor) } if let paceStatus { Text(paceStatus.statusText) .font(.caption.weight(.semibold)) .foregroundColor( isAchieved ? .appSuccess : (paceStatus.isBehind ? .negativeRed : .positiveGreen) ) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(12) .background(isAchieved ? Color.appSuccess.opacity(0.10) : Color.clear) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(isAchieved ? Color.appSuccess.opacity(0.35) : Color.clear, lineWidth: 1) ) .cornerRadius(12) } .buttonStyle(.plain) Button { showingShareOptions = true } label: { Image(systemName: "square.and.arrow.up") .foregroundColor(.appPrimary) .padding(.top, 2) } .buttonStyle(.borderless) } .padding(.vertical, 8) .confirmationDialog("Share Goal", isPresented: $showingShareOptions, titleVisibility: .visible) { Button("Share with amounts") { shareGoal(privacyMode: false) } Button("Share (privacy mode)") { shareGoal(privacyMode: true) } Button("Cancel", role: .cancel) {} } message: { Text("Choose how to share your goal progress") } } private func shareGoal(privacyMode: Bool) { GoalShareService.shared.shareGoal( name: goal.name, progress: progress, currentValue: totalValue, targetValue: goal.targetDecimal, targetDate: goal.targetDate, estimatedCompletionDate: estimatedCompletionDate, privacyMode: privacyMode ) } } #Preview { GoalsView() .environmentObject(AccountStore(iapService: IAPService())) }