import SwiftUI struct FamilyPlanView: View { @StateObject private var viewModel = FamilyPlanViewModel() @State private var showingInviteSheet = false @State private var selectedMetric: LeaderboardMetric = .distance var body: some View { NavigationView { Group { if viewModel.isLoading && viewModel.familyPlan == nil { loadingView } else if let plan = viewModel.familyPlan { planContent(plan) } else { emptyStateView } } .navigationTitle("Family Plan") .navigationBarTitleDisplayMode(.inline) .toolbar { if let plan = viewModel.familyPlan, plan.isActive { ToolbarItem(placement: .navigationBarTrailing) { Button { showingInviteSheet = true } label: { Text("Invite") } } } } .sheet(isPresented: $showingInviteSheet) { InviteMemberSheet() } } .onAppear { Task { await viewModel.fetchFamilyPlan() await viewModel.fetchLeaderboard() } } } @ViewBuilder private func planContent(_ plan: FamilyPlan) -> some View { List { Section("Plan Status") { HStack { VStack(alignment: .leading, spacing: 4) { Text(plan.ownerName) .font(.headline) Text("\(plan.members.count)/\(plan.maxMembers) members") .font(.subheadline) .foregroundColor(.secondary) } Spacer() Text(plan.subscriptionStatus.displayName) .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(plan.subscriptionStatus.color.opacity(0.2)) .cornerRadius(6) .foregroundColor(plan.subscriptionStatus.color) } if let renewalDate = plan.renewalDate { HStack { Text("Renews") .foregroundColor(.secondary) Spacer() Text(formatDate(renewalDate)) .fontWeight(.medium) } .font(.subheadline) } } Section("Members") { ForEach(plan.members) { member in MemberRowView(member: member) } } Section("Leaderboard") { Picker("Metric", selection: $selectedMetric) { ForEach(viewModel.metrics, id: \.self) { metric in Text(metric.displayName).tag(metric) } } .onChange(of: selectedMetric) { newValue in viewModel.selectedMetric = newValue Task { await viewModel.fetchLeaderboard() } } if viewModel.leaderboard.isEmpty { Text("No data yet") .foregroundColor(.secondary) .font(.subheadline) } else { ForEach(viewModel.leaderboard) { entry in LeaderboardRow(entry: entry, metric: selectedMetric) } } } } .listStyle(.insetGrouped) } private var loadingView: some View { VStack(spacing: 16) { ProgressView() Text("Loading Family Plan...") .font(.subheadline) .foregroundColor(.secondary) } .padding(.vertical, 60) } private var emptyStateView: some View { VStack(spacing: 16) { Image(systemName: "person.3.fill") .font(.system(size: 64)) .foregroundColor(.secondary) Text("No Family Plan") .font(.title2) .fontWeight(.semibold) Text("Create a family plan to share your subscription with up to 6 members.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 32) } .padding(.vertical, 60) } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .none return formatter.string(from: date) } } struct InviteMemberSheet: View { @Environment(\.dismiss) var dismiss @State private var email = "" @State private var name = "" var body: some View { NavigationView { Form { Section("New Member") { TextField("Name", text: $name) TextField("Email", text: $email) .keyboardType(.emailAddress) .autocapitalization(.none) } } .navigationTitle("Invite Member") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Send Invite") { dismiss() } .disabled(email.isEmpty || name.isEmpty) } } } } } struct MemberRowView: View { let member: FamilyMember var body: some View { HStack(spacing: 12) { ZStack { Circle() .fill(member.isPrimary ? Color.blue.opacity(0.2) : Color.secondary.opacity(0.15)) .frame(width: 40, height: 40) Image(systemName: member.role.icon) .font(.system(size: 18)) .foregroundColor(member.isPrimary ? .blue : .secondary) } VStack(alignment: .leading, spacing: 2) { Text(member.name) .font(.subheadline) .fontWeight(.medium) Text(member.email) .font(.caption) .foregroundColor(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text("\(Int(member.weeklyDistanceKm)) km") .font(.caption) .fontWeight(.medium) Text("this week") .font(.caption2) .foregroundColor(.secondary) } } .padding(.vertical, 4) } } struct LeaderboardRow: View { let entry: FamilyLeaderboardEntry let metric: LeaderboardMetric var body: some View { HStack(spacing: 12) { Text("#\(entry.rank)") .font(.headline) .frame(width: 30) Circle() .fill(Color.secondary.opacity(0.2)) .frame(width: 32, height: 32) Text(entry.memberName) .font(.subheadline) Spacer() Text("\(Int(entry.value))\(metric.unit)") .font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) } } } #Preview { FamilyPlanView() }