1.1.0 feature work: Monthly Check-in, Charts, Goals, Share, Reviews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-11 23:37:11 +01:00
parent daaca95913
commit 4761e2e5c8
19 changed files with 1118 additions and 90 deletions
+88 -8
View File
@@ -5,6 +5,7 @@ struct GoalsView: View {
@StateObject private var viewModel = GoalsViewModel()
@State private var showingAddGoal = false
@State private var editingGoal: Goal?
@State private var showAchievedGoals = false
var body: some View {
NavigationStack {
@@ -12,11 +13,15 @@ struct GoalsView: View {
AppBackground()
List {
if viewModel.goals.isEmpty {
emptyState
if filteredGoals.isEmpty {
if viewModel.goals.isEmpty {
emptyState
} else {
hiddenAchievedState
}
} else {
Section {
ForEach(viewModel.goals) { goal in
ForEach(filteredGoals) { goal in
GoalRowView(
goal: goal,
progress: viewModel.progress(for: goal),
@@ -48,6 +53,12 @@ struct GoalsView: View {
}
.navigationTitle("Goals")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(showAchievedGoals ? "Hide Achieved" : "Show Achieved") {
showAchievedGoals.toggle()
}
.font(.caption.weight(.semibold))
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddGoal = true
@@ -78,6 +89,11 @@ struct GoalsView: View {
}
}
private var filteredGoals: [Goal] {
guard !showAchievedGoals else { return viewModel.goals }
return viewModel.goals.filter { !viewModel.isAchieved($0) }
}
private var emptyState: some View {
VStack(spacing: 16) {
Image(systemName: "target")
@@ -93,6 +109,22 @@ struct GoalsView: View {
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
}
private var hiddenAchievedState: some View {
VStack(spacing: 12) {
Image(systemName: "party.popper")
.font(.system(size: 40))
.foregroundColor(.appSecondary)
Text("All visible goals are achieved")
.font(.headline)
Text("Tap \"Show Achieved\" to review completed goals.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
}
}
struct GoalRowView: View {
@@ -105,14 +137,53 @@ struct GoalRowView: View {
@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) {
Text(goal.name)
.font(.headline)
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: .appSecondary, iconColor: .appSecondary)
GoalProgressBar(
progress: progress,
tint: isAchieved ? .appSuccess : .appSecondary,
iconColor: isAchieved ? .appSuccess : .appSecondary
)
HStack {
Text(totalValue.currencyString)
@@ -126,16 +197,25 @@ struct GoalRowView: View {
if let targetDate = goal.targetDate {
Text("Target date: \(targetDate.mediumDateString)")
.font(.caption)
.foregroundColor(.secondary)
.foregroundColor(targetDateColor)
}
if let paceStatus {
Text(paceStatus.statusText)
.font(.caption.weight(.semibold))
.foregroundColor(paceStatus.isBehind ? .negativeRed : .positiveGreen)
.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)