1.5.0: aviso de huecos + resumen mensual compartible + import con vista previa
Feature 1 — Aviso de huecos: SnapshotGapDetector detecta huecos internos por source (meses sin datos entre dos snapshots). Banner desechable en Dashboard + DataGapsSheet para rellenar (abre AddSnapshotView con el mes que falta). Complementa la interpolación de gráficas: la línea se ve suave pero el usuario sabe que faltan datos reales. Feature 2 — Resumen mensual compartible: MonthlySummaryShareView (tarjeta con marca: valor, variación desde el check-in, racha, mood/rating) renderizada con ImageRenderer y compartida vía ShareService. Botón en la fila "check-in done" del Dashboard. Feature 3 — Import con vista previa: ImportPreview + previewImportAsync (dry-run READ-ONLY, no muta nada) cuenta qué se creará/actualizará usando los mismos checks de existencia que applyImport. ImportDataView muestra la confirmación antes de importar. El path real de import (applyImport/importDataAsync) NO se toca. Localización en los 7 idiomas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
This commit is contained in:
@@ -15,6 +15,10 @@ struct DashboardView: View {
|
||||
@State private var sectionConfigs = DashboardLayoutStore.load()
|
||||
@AppStorage("showForecast") private var showForecast = true
|
||||
@State private var showingQuickUpdate = false
|
||||
@State private var showingDataGaps = false
|
||||
/// Signature of the gap set last dismissed, so re-dismissal sticks until the
|
||||
/// gaps actually change (e.g. the user filled some).
|
||||
@AppStorage("dismissedDataGapsSignature") private var dismissedGapsSignature = ""
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
init() {
|
||||
@@ -39,9 +43,11 @@ struct DashboardView: View {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(visibleSections) { config in
|
||||
sectionView(for: config)
|
||||
if config.id == DashboardSection.monthlyCheckIn.id,
|
||||
!viewModel.insights.isEmpty {
|
||||
InsightsRow(insights: viewModel.insights)
|
||||
if config.id == DashboardSection.monthlyCheckIn.id {
|
||||
gapsBanner
|
||||
if !viewModel.insights.isEmpty {
|
||||
InsightsRow(insights: viewModel.insights)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +137,12 @@ struct DashboardView: View {
|
||||
.sheet(isPresented: $viewModel.showingPaywall) {
|
||||
PaywallView()
|
||||
}
|
||||
.sheet(isPresented: $showingDataGaps) {
|
||||
DataGapsSheet(
|
||||
gaps: viewModel.dataGaps,
|
||||
sourceProvider: { viewModel.source(withId: $0) }
|
||||
)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .openQuickUpdate)) { _ in
|
||||
showingQuickUpdate = true
|
||||
}
|
||||
@@ -180,6 +192,29 @@ struct DashboardView: View {
|
||||
sectionConfigs.filter { $0.isVisible }
|
||||
}
|
||||
|
||||
// MARK: - Data Gaps
|
||||
|
||||
/// A stable fingerprint of the current gap set — changes when gaps are
|
||||
/// added or filled, which re-shows a previously dismissed banner.
|
||||
private var currentGapsSignature: String {
|
||||
viewModel.dataGaps.map { $0.id }.sorted().joined(separator: "|")
|
||||
}
|
||||
|
||||
private var shouldShowGapsBanner: Bool {
|
||||
!viewModel.dataGaps.isEmpty && dismissedGapsSignature != currentGapsSignature
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var gapsBanner: some View {
|
||||
if shouldShowGapsBanner {
|
||||
DataGapsBanner(
|
||||
totalMissingMonths: viewModel.totalMissingMonths,
|
||||
onTap: { showingDataGaps = true },
|
||||
onDismiss: { dismissedGapsSignature = currentGapsSignature }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPad Layout Helpers
|
||||
|
||||
/// Full-width "hero" zones that lead the dashboard, in order.
|
||||
@@ -224,8 +259,11 @@ struct DashboardView: View {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(iPadLeadSections) { config in
|
||||
sectionView(for: config)
|
||||
if config.id == DashboardSection.monthlyCheckIn.id, !viewModel.insights.isEmpty {
|
||||
InsightsRow(insights: viewModel.insights)
|
||||
if config.id == DashboardSection.monthlyCheckIn.id {
|
||||
gapsBanner
|
||||
if !viewModel.insights.isEmpty {
|
||||
InsightsRow(insights: viewModel.insights)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +323,10 @@ struct DashboardView: View {
|
||||
lastUpdated: viewModel.formattedLastUpdate,
|
||||
pendingCount: viewModel.sourcesNeedingUpdate.count,
|
||||
totalSources: viewModel.totalSourceCount,
|
||||
streak: viewModel.updateStreak
|
||||
streak: viewModel.updateStreak,
|
||||
onShareSummary: {
|
||||
ShareService.shared.shareMonthlySummary(viewModel.monthlySummaryShareData())
|
||||
}
|
||||
)
|
||||
}
|
||||
case .momentumStreaks:
|
||||
@@ -557,6 +598,7 @@ struct MonthlyCheckInCard: View {
|
||||
var pendingCount: Int = 0
|
||||
var totalSources: Int = 0
|
||||
var streak: Int = 0
|
||||
var onShareSummary: (() -> Void)? = nil
|
||||
@State private var startDestinationActive = false
|
||||
@State private var showingGuidedFlow = false
|
||||
|
||||
@@ -734,6 +776,15 @@ struct MonthlyCheckInCard: View {
|
||||
.background(Color.orange.opacity(0.12))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
if let onShareSummary {
|
||||
Button(action: onShareSummary) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("Share monthly summary"))
|
||||
}
|
||||
Button {
|
||||
startDestinationActive = true
|
||||
} label: {
|
||||
|
||||
Reference in New Issue
Block a user