Feedback iPad: allocation grande + %, Home 2 columnas, YoY KPIs, achievements fix, iCloud calmado (build 58)

Allocation:
- Semicírculo responsive que llena su mitad de la fila (grande en iPad, no
  minúsculo en una esquina) + porcentaje dentro de cada segmento >=7%.
  SemicircleArc acepta center explícito; labels en el ángulo medio del arco.

Home iPad:
- Ya no columna única estrecha a 720. Hero + check-in a ancho completo, resto
  de cards en DOS columnas masonry independientes (sin acoplar alturas → sin
  gaps del grid). Cap 1100pt.

Year vs Year KPIs:
- Retorno del año actual + diferencia (pp) vs año anterior + mejor/peor año
  histórico. Usa forecastEndValue para el año en curso.

Fixes:
- 'View all achievements' en Journal no hacía nada: NavigationLink inline
  (frágil según contexto de navegación) → sheet fiable en cualquier sitio.
- iCloud: CKErrorPartialFailure tras una sync que funciona ya NO se pinta en
  rojo alarmante — nota calmada 'Sincronizando con iCloud' + detalle técnico
  en disclosure. Rojo solo si nunca ha sincronizado y es fallo total.
  syncErrorIsCritical + strings ×7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
alexandrev-tibco
2026-07-10 14:48:06 +02:00
parent 18f2609d87
commit 1abffdb7bb
13 changed files with 221 additions and 48 deletions
@@ -416,13 +416,40 @@ struct ChartsContainerView: View {
color: end >= 0 ? .positiveGreen : .negativeRed)
}
case .yearOverYear:
let years = viewModel.yearOverYearData.filter { viewModel.yoySelectedYears.contains($0.year) }
guard !years.isEmpty else { return [] }
return years.map { ys in
let end = ys.values.compactMap { $0.isNaN ? nil : $0 }.last ?? 0
return ChartStat(label: "\(ys.year)", value: String(format: "%+.1f%%", end),
color: end >= 0 ? .positiveGreen : .negativeRed)
let all = viewModel.yearOverYearData.sorted { $0.year < $1.year }
guard let latest = all.last else { return [] }
func endReturn(_ ys: ChartsViewModel.YearSeries) -> Double {
ys.forecastEndValue ?? (ys.values.compactMap { $0.isNaN ? nil : $0 }.last ?? 0)
}
let latestEnd = endReturn(latest)
var stats: [ChartStat] = [
ChartStat(label: "\(latest.year)", value: String(format: "%+.1f%%", latestEnd),
color: latestEnd >= 0 ? .positiveGreen : .negativeRed)
]
// Current year vs previous year (end-of-year return difference).
if all.count >= 2 {
let prev = all[all.count - 2]
let diff = latestEnd - endReturn(prev)
stats.append(ChartStat(
label: String(format: String(localized: "yoy_vs_prev"), "\(prev.year)"),
value: String(format: "%+.1f pp", diff),
color: diff >= 0 ? .positiveGreen : .negativeRed))
}
// Best and worst full year on record.
if all.count >= 2 {
let ranked = all.map { ($0.year, endReturn($0)) }.sorted { $0.1 > $1.1 }
if let best = ranked.first {
stats.append(ChartStat(label: String(localized: "yoy_best_year"),
value: "\(best.0) \(String(format: "%+.1f%%", best.1))",
color: .positiveGreen))
}
if let worst = ranked.last, worst.0 != ranked.first?.0 {
stats.append(ChartStat(label: String(localized: "yoy_worst_year"),
value: "\(worst.0) \(String(format: "%+.1f%%", worst.1))",
color: .negativeRed))
}
}
return stats
case .simulator:
// The simulator carries its own live sliders and comparison inline.
return []