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:
@@ -98,6 +98,7 @@ struct MonthlyCheckInView: View {
|
||||
VStack(spacing: 20) {
|
||||
headerCard
|
||||
summaryCard
|
||||
monthlyHighlightsCard
|
||||
reflectionCard
|
||||
sourcesCard
|
||||
notesCard
|
||||
@@ -418,6 +419,73 @@ struct MonthlyCheckInView: View {
|
||||
.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 {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
|
||||
Reference in New Issue
Block a user