1.4.1 (build 42): contribución por snapshot, fixes Period vs Period y navegación iPad
- 151: contribución editable por snapshot en cualquier modo (quitado gate detailed) con diálogo de propagación al editar: solo este / adelante / atrás / todos (SnapshotRepository.propagateContribution, SnapshotFormViewModel.contributionChanged) - 148: Period vs Period mostraba mal el último mes del period B — la agrupación usaba chartMonth(for:) que aplicaba el grace-period del check-in a snapshots históricos; ahora agrupa por mes calendario crudo - 152: iPad Sources no saltaba entre fuentes — añadido .id() al SourceDetailView para recrear el StateObject al cambiar de selección - 146/147: Year vs Year con forecast del año en curso (asterisco de estimado) y KPIs arriba / detalle debajo - 145: card de contribución mensual en SourceDetailView - Nuevas claves snapshot_contribution_propagate_* en los 7 idiomas Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>1.4.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>42</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -74,6 +74,8 @@ struct InvestmentWidgetEntry: TimelineEntry {
|
||||
let categoryTotals: [(name: String, value: Decimal, color: String)]
|
||||
let goals: [GoalSummary]
|
||||
let currencyCode: String
|
||||
let insightTitle: String
|
||||
let insightValue: String
|
||||
}
|
||||
|
||||
struct CategorySeries: Identifiable {
|
||||
@@ -139,7 +141,9 @@ struct InvestmentWidgetProvider: TimelineProvider {
|
||||
goals: [
|
||||
GoalSummary(name: "Target", targetAmount: 75000, targetDate: nil)
|
||||
],
|
||||
currencyCode: "EUR"
|
||||
currencyCode: "EUR",
|
||||
insightTitle: "Milestone ahead",
|
||||
insightValue: "€5K from €50K"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,7 +180,9 @@ struct InvestmentWidgetProvider: TimelineProvider {
|
||||
categoryEvolution: [],
|
||||
categoryTotals: [],
|
||||
goals: [],
|
||||
currencyCode: currencyCode
|
||||
currencyCode: currencyCode,
|
||||
insightTitle: "",
|
||||
insightValue: ""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -362,6 +368,41 @@ struct InvestmentWidgetProvider: TimelineProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Compute top insight
|
||||
var insightTitle = ""
|
||||
var insightValue = ""
|
||||
|
||||
// Milestone approaching
|
||||
let milestones: [Double] = [1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1_000_000]
|
||||
let totalDouble = NSDecimalNumber(decimal: totalValue).doubleValue
|
||||
if let nextMs = milestones.first(where: { $0 > totalDouble }) {
|
||||
let pct = totalDouble / nextMs
|
||||
if pct >= 0.90 {
|
||||
let gap = nextMs - totalDouble
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.currencyCode = currencyCode
|
||||
formatter.maximumFractionDigits = 0
|
||||
let msStr = formatter.string(from: NSNumber(value: nextMs)) ?? ""
|
||||
let gapStr = formatter.string(from: NSNumber(value: gap)) ?? ""
|
||||
insightTitle = "Milestone ahead"
|
||||
insightValue = "\(gapStr) from \(msStr)"
|
||||
}
|
||||
}
|
||||
|
||||
// Year performance (only if no milestone insight)
|
||||
if insightTitle.isEmpty && !trendPoints.isEmpty && trendPoints.count >= 2 {
|
||||
let first = NSDecimalNumber(decimal: trendPoints.first!).doubleValue
|
||||
let last = NSDecimalNumber(decimal: trendPoints.last!).doubleValue
|
||||
if first > 0 {
|
||||
let pct = (last - first) / first * 100
|
||||
if abs(pct) >= 1 {
|
||||
insightTitle = "Year to date"
|
||||
insightValue = String(format: "%+.1f%%", pct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InvestmentWidgetEntry(
|
||||
date: Date(),
|
||||
isPremium: isPremium,
|
||||
@@ -374,7 +415,9 @@ struct InvestmentWidgetProvider: TimelineProvider {
|
||||
categoryEvolution: categoryEvolution,
|
||||
categoryTotals: categoryTotalsData.map { (name: $0.name, value: $0.value, color: $0.color) },
|
||||
goals: goals,
|
||||
currencyCode: currencyCode
|
||||
currencyCode: currencyCode,
|
||||
insightTitle: insightTitle,
|
||||
insightValue: insightValue
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -498,6 +541,23 @@ struct MediumWidgetView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !entry.insightTitle.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "lightbulb.fill")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.orange)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(entry.insightTitle)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text(entry.insightValue)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -930,7 +990,9 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
|
||||
categoryEvolution: [],
|
||||
categoryTotals: [],
|
||||
goals: [],
|
||||
currencyCode: "EUR"
|
||||
currencyCode: "EUR",
|
||||
insightTitle: "",
|
||||
insightValue: ""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -953,7 +1015,9 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
|
||||
categoryEvolution: [],
|
||||
categoryTotals: [],
|
||||
goals: [],
|
||||
currencyCode: "EUR"
|
||||
currencyCode: "EUR",
|
||||
insightTitle: "Year to date",
|
||||
insightValue: "+12.4%"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1004,7 +1068,9 @@ struct PortfolioJournalWidgetBundle: WidgetBundle {
|
||||
goals: [
|
||||
GoalSummary(name: "Target", targetAmount: 120000, targetDate: nil)
|
||||
],
|
||||
currencyCode: "EUR"
|
||||
currencyCode: "EUR",
|
||||
insightTitle: "Milestone ahead",
|
||||
insightValue: "€5K from €100K"
|
||||
)
|
||||
}
|
||||
extension Decimal {
|
||||
|
||||
Reference in New Issue
Block a user