156 lines
5.7 KiB
Swift
156 lines
5.7 KiB
Swift
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?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
AppBackground()
|
|
|
|
List {
|
|
if viewModel.goals.isEmpty {
|
|
emptyState
|
|
} else {
|
|
Section {
|
|
ForEach(viewModel.goals) { goal in
|
|
GoalRowView(
|
|
goal: goal,
|
|
progress: viewModel.progress(for: goal),
|
|
totalValue: viewModel.totalValue(for: goal),
|
|
paceStatus: viewModel.paceStatus(for: goal)
|
|
)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
editingGoal = goal
|
|
}
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
|
Button(role: .destructive) {
|
|
viewModel.deleteGoal(goal)
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
.swipeActions(edge: .leading, allowsFullSwipe: false) {
|
|
Button {
|
|
editingGoal = goal
|
|
} label: {
|
|
Label("Edit", systemImage: "pencil")
|
|
}
|
|
.tint(.appSecondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
}
|
|
.navigationTitle("Goals")
|
|
.toolbar {
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emptyState: some View {
|
|
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?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text(goal.name)
|
|
.font(.headline)
|
|
Spacer()
|
|
Button {
|
|
GoalShareService.shared.shareGoal(
|
|
name: goal.name,
|
|
progress: progress,
|
|
currentValue: totalValue,
|
|
targetValue: goal.targetDecimal
|
|
)
|
|
} label: {
|
|
Image(systemName: "square.and.arrow.up")
|
|
.foregroundColor(.appPrimary)
|
|
}
|
|
}
|
|
|
|
GoalProgressBar(progress: progress, tint: .appSecondary, iconColor: .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(.secondary)
|
|
}
|
|
|
|
if let paceStatus {
|
|
Text(paceStatus.statusText)
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundColor(paceStatus.isBehind ? .negativeRed : .positiveGreen)
|
|
}
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
GoalsView()
|
|
.environmentObject(AccountStore(iapService: IAPService()))
|
|
}
|