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:
@@ -0,0 +1,139 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Stat definition
|
||||
struct ChartStat {
|
||||
let label: String
|
||||
let value: String
|
||||
let color: Color
|
||||
}
|
||||
|
||||
// MARK: - Stats summary row (horizontal scroll of chips)
|
||||
struct ChartStatsRow: View {
|
||||
let stats: [ChartStat]
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(stats.indices, id: \.self) { i in
|
||||
VStack(alignment: .center, spacing: 3) {
|
||||
Text(stats[i].label)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text(stats[i].value)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(stats[i].color)
|
||||
}
|
||||
.frame(minWidth: 60)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data table row model
|
||||
struct ChartDataTableRow: Identifiable {
|
||||
let id = UUID()
|
||||
let label: String
|
||||
let value: String
|
||||
let deltaPrev: String?
|
||||
let deltaFirst: String?
|
||||
let isPrevPositive: Bool
|
||||
let isFirstPositive: Bool
|
||||
}
|
||||
|
||||
// MARK: - Expandable data table
|
||||
struct ChartDataTable: View {
|
||||
let rows: [ChartDataTableRow]
|
||||
let valueHeader: String
|
||||
let deltaPrevHeader: String?
|
||||
let deltaFirstHeader: String?
|
||||
|
||||
@State private var isExpanded = false
|
||||
private let previewCount = 5
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
tableHeader
|
||||
Divider()
|
||||
let displayed = isExpanded ? rows : Array(rows.suffix(previewCount))
|
||||
ForEach(displayed) { row in
|
||||
tableDataRow(row)
|
||||
if row.id != displayed.last?.id {
|
||||
Divider().opacity(0.35)
|
||||
}
|
||||
}
|
||||
if rows.count > previewCount {
|
||||
Divider().opacity(0.35)
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.15)) { isExpanded.toggle() }
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text(isExpanded ? "Show less" : "Show all \(rows.count)")
|
||||
.font(.caption)
|
||||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||||
.font(.caption2)
|
||||
}
|
||||
.foregroundColor(.appPrimary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
private var tableHeader: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text("Date").font(.caption2.weight(.semibold)).foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(valueHeader).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
|
||||
.frame(width: 72, alignment: .trailing)
|
||||
if let h = deltaPrevHeader {
|
||||
Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
|
||||
.frame(width: 62, alignment: .trailing)
|
||||
}
|
||||
if let h = deltaFirstHeader {
|
||||
Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
|
||||
.frame(width: 62, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
|
||||
private func tableDataRow(_ row: ChartDataTableRow) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Text(row.label).font(.caption2).foregroundColor(.primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text(row.value).font(.caption2.weight(.medium))
|
||||
.frame(width: 72, alignment: .trailing)
|
||||
if let dp = row.deltaPrev, deltaPrevHeader != nil {
|
||||
Text(dp).font(.caption2.weight(.medium))
|
||||
.foregroundColor(row.isPrevPositive ? .positiveGreen : .negativeRed)
|
||||
.frame(width: 62, alignment: .trailing)
|
||||
}
|
||||
if let df = row.deltaFirst, deltaFirstHeader != nil {
|
||||
Text(df).font(.caption2.weight(.medium))
|
||||
.foregroundColor(row.isFirstPositive ? .positiveGreen : .negativeRed)
|
||||
.frame(width: 62, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Compact date formatter
|
||||
private let tableMonthFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.setLocalizedDateFormatFromTemplate("MMMy")
|
||||
return f
|
||||
}()
|
||||
|
||||
func chartTableDateLabel(_ date: Date) -> String {
|
||||
tableMonthFormatter.string(from: date)
|
||||
}
|
||||
Reference in New Issue
Block a user