Add monthly highlights: best and worst performing sources

Shows a highlights card with the best and worst performing sources by
percentage change, displayed between the summary and reflection cards.

Fixes #22

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-02-11 23:49:00 +01:00
parent aceed608ed
commit 5dc9eb109f
@@ -98,6 +98,7 @@ struct MonthlyCheckInView: View {
VStack(spacing: 20) { VStack(spacing: 20) {
headerCard headerCard
summaryCard summaryCard
monthlyHighlightsCard
reflectionCard reflectionCard
sourcesCard sourcesCard
notesCard notesCard
@@ -418,6 +419,73 @@ struct MonthlyCheckInView: View {
.shadow(color: .black.opacity(0.05), radius: 8, y: 2) .shadow(color: .black.opacity(0.05), radius: 8, y: 2)
} }
private var sourcePerformances: [(name: String, diff: Decimal, percentage: Double)] {
viewModel.sources.compactMap { source -> (name: String, diff: Decimal, percentage: Double)? in
let snapshots = source.sortedSnapshotsByDateAscending
guard snapshots.count >= 2 else { return nil }
let latest = snapshots[snapshots.count - 1]
let previous = snapshots[snapshots.count - 2]
let diff = latest.decimalValue - previous.decimalValue
let pct = previous.decimalValue > 0
? NSDecimalNumber(decimal: diff / previous.decimalValue * 100).doubleValue
: 0
return (name: source.name, diff: diff, percentage: pct)
}
}
@ViewBuilder
private var monthlyHighlightsCard: some View {
let perfs = sourcePerformances
if perfs.count >= 2 {
let best = perfs.max(by: { $0.percentage < $1.percentage })
let worst = perfs.min(by: { $0.percentage < $1.percentage })
VStack(alignment: .leading, spacing: 12) {
Text("Monthly Highlights")
.font(.headline)
if let best {
HStack {
Image(systemName: "arrow.up.circle.fill")
.foregroundColor(.positiveGreen)
VStack(alignment: .leading, spacing: 2) {
Text("Best Performer")
.font(.caption)
.foregroundColor(.secondary)
Text("\(best.name)")
.font(.subheadline.weight(.semibold))
}
Spacer()
Text(String(format: "%+.1f%%", best.percentage))
.font(.subheadline.weight(.bold))
.foregroundColor(.positiveGreen)
}
}
if let worst, worst.name != best?.name {
HStack {
Image(systemName: "arrow.down.circle.fill")
.foregroundColor(.negativeRed)
VStack(alignment: .leading, spacing: 2) {
Text("Worst Performer")
.font(.caption)
.foregroundColor(.secondary)
Text("\(worst.name)")
.font(.subheadline.weight(.semibold))
}
Spacer()
Text(String(format: "%+.1f%%", worst.percentage))
.font(.subheadline.weight(.bold))
.foregroundColor(.negativeRed)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
private var sourcesCard: some View { private var sourcesCard: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack { HStack {