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) {
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user