import SwiftUI /// Three vertical pages: portfolio summary, goals, and check-in status. The /// watch app is read-only — every edit still happens on the iPhone. struct WatchRootView: View { @EnvironmentObject private var store: WatchDataStore var body: some View { TabView { WatchSummaryView(snapshot: store.snapshot) WatchGoalsView(snapshot: store.snapshot) WatchCheckInView(snapshot: store.snapshot) } .tabViewStyle(.verticalPage) } } // MARK: - Summary struct WatchSummaryView: View { let snapshot: WatchPortfolioSnapshot @EnvironmentObject private var store: WatchDataStore var body: some View { ScrollView { VStack(alignment: .leading, spacing: 10) { if !snapshot.hasData { WatchEmptyStateView() } else { VStack(alignment: .leading, spacing: 2) { Text("watch_total_value") .font(.caption2) .foregroundStyle(.secondary) Text(snapshot.formattedTotal()) .font(.title3.bold()) .minimumScaleFactor(0.6) .lineLimit(1) } HStack(spacing: 6) { Image(systemName: snapshot.monthChange >= 0 ? "arrow.up.right" : "arrow.down.right") Text(snapshot.formattedMonthChange()) Text(snapshot.formattedMonthChangePercent) .foregroundStyle(.secondary) } .font(.caption) .foregroundStyle(snapshot.monthChange >= 0 ? Color.green : Color.red) if !snapshot.sources.isEmpty { Divider() ForEach(snapshot.sources) { source in HStack(spacing: 6) { Circle() .fill(Color(watchHex: source.categoryColorHex) ?? .gray) .frame(width: 6, height: 6) Text(source.name) .font(.caption2) .lineLimit(1) Spacer(minLength: 4) Text(WatchPortfolioSnapshot.format( source.value, currencyCode: snapshot.currencyCode, compact: true )) .font(.caption2.weight(.semibold)) .foregroundStyle(.secondary) } } } WatchFooterView(snapshot: snapshot) } } .padding(.horizontal, 2) } .navigationTitle("watch_tab_summary") } } // MARK: - Goals struct WatchGoalsView: View { let snapshot: WatchPortfolioSnapshot var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { Text("watch_goals_title") .font(.headline) if snapshot.goals.isEmpty { Text("watch_no_goals") .font(.caption2) .foregroundStyle(.secondary) } else { ForEach(snapshot.goals) { goal in VStack(alignment: .leading, spacing: 4) { HStack(spacing: 4) { if let icon = goal.categoryIcon { Image(systemName: icon) .font(.caption2) .foregroundStyle(Color(watchHex: goal.categoryColorHex) ?? .accentColor) } Text(goal.name) .font(.caption.weight(.semibold)) .lineLimit(1) } ProgressView(value: goal.progress) .tint(goal.isAchieved ? .green : (Color(watchHex: goal.categoryColorHex) ?? .accentColor)) HStack { Text(WatchPortfolioSnapshot.format( goal.currentValue, currencyCode: snapshot.currencyCode, compact: true )) Text(String( format: String(localized: "watch_of_target"), WatchPortfolioSnapshot.format( goal.targetValue, currencyCode: snapshot.currencyCode, compact: true ) )) .foregroundStyle(.secondary) Spacer(minLength: 2) Text("\(Int(goal.progress * 100))%") .foregroundStyle(.secondary) } .font(.caption2) } } } } .padding(.horizontal, 2) } } } // MARK: - Check-in struct WatchCheckInView: View { let snapshot: WatchPortfolioSnapshot var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { Text("watch_checkin_title") .font(.headline) HStack(spacing: 8) { Image(systemName: snapshot.checkInDone ? "checkmark.circle.fill" : "exclamationmark.circle.fill") .foregroundStyle(snapshot.checkInDone ? Color.green : Color.orange) VStack(alignment: .leading, spacing: 2) { Text(snapshot.checkInDone ? "watch_checkin_done" : "watch_checkin_pending") .font(.caption.weight(.semibold)) if !snapshot.checkInMonthLabel.isEmpty { Text(snapshot.checkInMonthLabel) .font(.caption2) .foregroundStyle(.secondary) } } } HStack(spacing: 12) { WatchStatTile( title: String(localized: "watch_streak"), value: "\(snapshot.currentStreak)x" ) WatchStatTile( title: String(localized: "watch_best_streak"), value: "\(snapshot.bestStreak)x" ) } } .padding(.horizontal, 2) } } } // MARK: - Shared pieces struct WatchStatTile: View { let title: String let value: String var body: some View { VStack(alignment: .leading, spacing: 2) { Text(title) .font(.caption2) .foregroundStyle(.secondary) Text(value) .font(.title3.bold()) } .frame(maxWidth: .infinity, alignment: .leading) .padding(8) .background(.quaternary, in: RoundedRectangle(cornerRadius: 10)) } } struct WatchEmptyStateView: View { var body: some View { VStack(spacing: 8) { Image(systemName: "iphone.and.arrow.forward") .font(.title2) .foregroundStyle(.secondary) Text("watch_no_data_title") .font(.caption.weight(.semibold)) Text("watch_no_data_body") .font(.caption2) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(.vertical, 8) } } /// Freshness line plus a manual pull, for when the user wants to be sure the /// number on the wrist is the one on the phone. struct WatchFooterView: View { let snapshot: WatchPortfolioSnapshot @EnvironmentObject private var store: WatchDataStore var body: some View { VStack(spacing: 6) { Text(String( format: String(localized: "watch_updated"), snapshot.generatedAt.formatted(date: .omitted, time: .shortened) )) .font(.caption2) .foregroundStyle(.secondary) if store.isPhoneReachable { Button { store.refresh() } label: { if store.isRefreshing { ProgressView() } else { Label("watch_refresh", systemImage: "arrow.clockwise") .font(.caption2) } } .buttonStyle(.bordered) } } .frame(maxWidth: .infinity) .padding(.top, 4) } }