import SwiftUI import UIKit struct SettingsView: View { @State private var store = AppStore.shared @State private var session = SessionStore.shared @State private var ssoProviders: VelocitySSOProvidersDTO? @State private var mdmConfig: VelocityMDMConfigDTO? @State private var tenantUsers: [VelocityAuthUserDTO] = [] @State private var identityMessage: String? @State private var identityError: String? @State private var isSwitchingSession = false @State private var isAdvancedConfigurationUnlocked = false @AppStorage("velocity.notifications.clientInsights") private var clientInsightNotifications = true @AppStorage("velocity.notifications.calendar") private var calendarNotifications = true @AppStorage("velocity.notifications.showroom") private var showroomNotifications = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 24) { VStack(alignment: .leading, spacing: 4) { Text("Settings") .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Profile and notification preferences") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } profileSection notificationPreferencesSection if isAdvancedConfigurationUnlocked { SettingsSection(title: "Connectivity") { SettingsRow( label: "Backend endpoint", value: session.endpointDisplay, icon: "server.rack", accentColor: VelocityTheme.accent ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Dream Weaver endpoint", value: session.dreamWeaverEndpointDisplay, icon: "wand.and.stars", accentColor: session.dreamWeaverEndpointModeDescription == "Dedicated gateway" ? VelocityTheme.warning : VelocityTheme.mutedFg ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Dream Weaver route mode", value: session.dreamWeaverEndpointModeDescription, icon: "point.3.connected.trianglepath.dotted", accentColor: session.dreamWeaverEndpointModeDescription == "Dedicated gateway" ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Dream Weaver auth", value: session.dreamWeaverAuthenticationDescription, icon: "key.horizontal", accentColor: session.dreamWeaverAuthenticationDescription == "API key configured" ? VelocityTheme.success : VelocityTheme.mutedFg ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Auth mode", value: session.authModeDescription, icon: "lock.shield", accentColor: session.isConfigured ? VelocityTheme.success : VelocityTheme.warning ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Config source", value: session.configurationSourceDescription, icon: "externaldrive.badge.icloud", accentColor: session.isUsingStoredRuntimeConfiguration ? VelocityTheme.success : VelocityTheme.mutedFg ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Last refresh", value: store.lastRefreshAt?.relativeShort ?? "No live fetch yet", icon: "arrow.clockwise", accentColor: VelocityTheme.mutedFg ) } SettingsSection(title: "Operator") { SettingsRow( label: "Identity", value: session.operatorIdentity, icon: "person.crop.circle", accentColor: VelocityTheme.accent ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "CRM contacts loaded", value: "\(store.contacts.count)", icon: "person.3", accentColor: VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Pending CRM tasks loaded", value: "\(store.tasks.count)", icon: "checklist", accentColor: VelocityTheme.warning ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Property records loaded", value: "\(store.properties.count)", icon: "building.2", accentColor: VelocityTheme.warning ) } SettingsSection(title: "Enterprise Identity") { SettingsRow( label: "SSO providers", value: ssoProviders?.providers.map(\.name).joined(separator: ", ") ?? "Not loaded", icon: "person.badge.key", accentColor: ssoProviders?.enabled == true ? VelocityTheme.success : VelocityTheme.warning ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "MDM configuration", value: mdmConfig?.managedConfigurationRequired == true ? "Required · \(mdmConfig?.configurationKeys.count ?? 0) keys" : "Optional · \(mdmConfig?.configurationKeys.count ?? 0) keys", icon: "iphone.badge.gearshape", accentColor: mdmConfig == nil ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Tenant users", value: tenantUsers.isEmpty ? "Not loaded" : "\(tenantUsers.count) available", icon: "person.2.badge.gearshape", accentColor: tenantUsers.isEmpty ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) VStack(alignment: .leading, spacing: 10) { HStack(spacing: 10) { Button { Task { await requestPasswordRecovery() } } label: { Label("Request Recovery", systemImage: "lock.rotation") } .buttonStyle(.borderedProminent) .tint(VelocityTheme.accent) Button { Task { await loadEnterpriseIdentity() } } label: { Label("Refresh Identity", systemImage: "arrow.clockwise") } .buttonStyle(.bordered) .tint(VelocityTheme.accent) Menu { if tenantUsers.isEmpty { Text("No tenant users returned") } else { ForEach(tenantUsers) { user in Button { Task { await switchSession(to: user) } } label: { Label("\(user.displayName) · \(user.role)", systemImage: "person.crop.circle.badge.checkmark") } } } } label: { Label(isSwitchingSession ? "Switching..." : "Switch User", systemImage: "person.2") } .buttonStyle(.bordered) .tint(VelocityTheme.accent) .disabled(isSwitchingSession || tenantUsers.isEmpty) } if let identityMessage { Text(identityMessage) .font(.system(size: 12, weight: .medium)) .foregroundStyle(VelocityTheme.success) } if let identityError { Text(identityError) .font(.system(size: 12, weight: .medium)) .foregroundStyle(VelocityTheme.danger) } } .padding(.horizontal, 16) .padding(.vertical, 12) } SettingsSection(title: "Production Readiness") { SettingsRow( label: "Canonical contacts", value: "\(store.contacts.count) loaded", icon: "person.text.rectangle", accentColor: store.contacts.isEmpty ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Pipeline lanes", value: "\(store.kanbanColumns.reduce(0) { $0 + $1.count }) leads", icon: "square.grid.3x1.below.line.grid.1x2", accentColor: store.kanbanColumns.isEmpty ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Deals", value: "\(store.opportunities.count) opportunities", icon: "target", accentColor: store.opportunities.isEmpty ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Timeline events", value: "\(store.timelineEvents.count) hydrated", icon: "clock.arrow.circlepath", accentColor: store.timelineEvents.isEmpty ? VelocityTheme.warning : VelocityTheme.success ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Last app error", value: store.errorMessage ?? "None", icon: "exclamationmark.triangle", accentColor: store.errorMessage == nil ? VelocityTheme.success : VelocityTheme.danger ) } SessionConfigurationPanel( title: "Session Configuration", subtitle: "Update the production endpoint, point Dream Weaver at a dedicated gateway when needed, or rotate operator credentials without rebuilding the app. Saving clears the cached token, re-runs a live refresh, and probes the Dream Weaver routes.", primaryActionTitle: "Save and refresh", allowsClearingStoredConfiguration: true ) SettingsSection(title: "Production Notes") { VStack(alignment: .leading, spacing: 8) { Text("This build avoids local demo data. Runtime session overrides are stored on-device so investor or operator installs no longer depend on committed build-time credentials.") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.foreground) Text("\(SentinelScope.navigationTitle) now reads persisted perception analytics from the production Sentinel stream; Communications, Calendar, Dashboard, Oracle, and inventory media are live-backed. Dream Weaver can use a dedicated gateway with an optional per-gateway API key, and backend plus generation route health are enforced and reported truthfully.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(.horizontal, 16) .padding(.vertical, 14) } } Spacer(minLength: 24) } .padding(24) .frame(maxWidth: .infinity, alignment: .topLeading) } .overlay(alignment: .topTrailing) { ThreeFingerLongPressGate { withAnimation(.interactiveSpring(response: 0.45, dampingFraction: 0.86)) { isAdvancedConfigurationUnlocked = true } } .frame(width: 180, height: 180) .allowsHitTesting(!isAdvancedConfigurationUnlocked) } .scrollIndicators(.visible) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .background(VelocityTheme.background) .task { if isAdvancedConfigurationUnlocked { await loadEnterpriseIdentity() } } .onChange(of: isAdvancedConfigurationUnlocked) { _, unlocked in guard unlocked else { return } Task { await loadEnterpriseIdentity() } } } private var profileSection: some View { SettingsSection(title: "Profile") { SettingsRow( label: "Signed in", value: session.operatorIdentity, icon: "person.crop.circle", accentColor: VelocityTheme.accent ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Session", value: session.authModeDescription, icon: "lock.shield", accentColor: session.isConfigured ? VelocityTheme.success : VelocityTheme.warning ) Divider().background(VelocityTheme.borderSubtle) SettingsRow( label: "Showroom privacy", value: store.isShowroomModeEnabled ? "Buyer-safe" : "Broker private", icon: store.isShowroomModeEnabled ? "eye.slash" : "eye", accentColor: store.isShowroomModeEnabled ? VelocityTheme.warning : VelocityTheme.success ) } } private var notificationPreferencesSection: some View { SettingsSection(title: "Notifications") { ToggleRow( label: "Client insight alerts", detail: "Private broker recommendations", icon: "sparkles", accentColor: VelocityTheme.accent, isOn: $clientInsightNotifications ) Divider().background(VelocityTheme.borderSubtle) ToggleRow( label: "Calendar reminders", detail: "Confirmed events and follow-ups", icon: "calendar.badge.clock", accentColor: VelocityTheme.warning, isOn: $calendarNotifications ) Divider().background(VelocityTheme.borderSubtle) ToggleRow( label: "Showroom mode changes", detail: "Buyer-safe privacy transitions", icon: "eye.slash", accentColor: VelocityTheme.success, isOn: $showroomNotifications ) } } private func loadEnterpriseIdentity() async { do { async let providers = VelocityAPIClient.shared.fetchSSOProviders() async let mdm = VelocityAPIClient.shared.fetchMDMConfig() async let users = VelocityAPIClient.shared.fetchAuthUsers() let resolvedProviders = try await providers let resolvedMDM = try await mdm let resolvedUsers = (try? await users) ?? [] await MainActor.run { ssoProviders = resolvedProviders mdmConfig = resolvedMDM tenantUsers = resolvedUsers identityError = nil } } catch { await MainActor.run { identityError = error.localizedDescription } } } private func requestPasswordRecovery() async { do { try await VelocityAPIClient.shared.requestPasswordRecovery(email: session.operatorIdentity) await MainActor.run { identityMessage = "Password recovery request recorded for \(session.operatorIdentity)." identityError = nil } } catch { await MainActor.run { identityError = error.localizedDescription identityMessage = nil } } } private func switchSession(to user: VelocityAuthUserDTO) async { await MainActor.run { isSwitchingSession = true identityError = nil identityMessage = nil } do { let result = try await VelocityAPIClient.shared.requestSessionSwitch(userId: user.userId) await store.refresh(silent: true) await MainActor.run { isSwitchingSession = false identityMessage = result.requiresReauthentication ? "Session switch approved for \(user.displayName); reauthentication is required." : "Session switched to \(user.displayName)." } } catch { await MainActor.run { isSwitchingSession = false identityError = error.localizedDescription } } } } private struct SettingsSection: View { let title: String @ViewBuilder let content: Content var body: some View { VStack(alignment: .leading, spacing: 0) { Text(title.uppercased()) .font(.system(size: 10, weight: .semibold)) .tracking(1.2) .foregroundStyle(VelocityTheme.mutedFg) .padding(.bottom, 8) .padding(.horizontal, 4) VStack(spacing: 0) { content } .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 14) .fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderSubtle, lineWidth: 1) ) ) } } } private struct SettingsRow: View { let label: String let value: String let icon: String let accentColor: Color var body: some View { HStack(spacing: 14) { ZStack { RoundedRectangle(cornerRadius: 7) .fill(accentColor.opacity(0.12)) .frame(width: 30, height: 30) Image(systemName: icon) .font(.system(size: 13, weight: .medium)) .foregroundStyle(accentColor) } Text(label) .font(.system(size: 14)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text(value) .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) .multilineTextAlignment(.trailing) } .padding(.horizontal, 16) .padding(.vertical, 12) } } private struct ToggleRow: View { let label: String let detail: String let icon: String let accentColor: Color @Binding var isOn: Bool var body: some View { Toggle(isOn: $isOn) { HStack(spacing: 14) { ZStack { RoundedRectangle(cornerRadius: 7) .fill(accentColor.opacity(0.12)) .frame(width: 30, height: 30) Image(systemName: icon) .font(.system(size: 13, weight: .medium)) .foregroundStyle(accentColor) } VStack(alignment: .leading, spacing: 2) { Text(label) .font(.system(size: 14)) .foregroundStyle(VelocityTheme.foreground) Text(detail) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } } } .toggleStyle(.switch) .tint(VelocityTheme.accent) .padding(.horizontal, 16) .padding(.vertical, 12) } } private struct ThreeFingerLongPressGate: UIViewRepresentable { let onUnlock: () -> Void func makeUIView(context: Context) -> UIView { let view = UIView(frame: .zero) view.backgroundColor = .clear let recognizer = UILongPressGestureRecognizer( target: context.coordinator, action: #selector(Coordinator.didLongPress(_:)) ) recognizer.minimumPressDuration = 1.15 recognizer.numberOfTouchesRequired = 3 view.addGestureRecognizer(recognizer) return view } func updateUIView(_ uiView: UIView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(onUnlock: onUnlock) } final class Coordinator: NSObject { let onUnlock: () -> Void init(onUnlock: @escaping () -> Void) { self.onUnlock = onUnlock } @objc func didLongPress(_ recognizer: UILongPressGestureRecognizer) { guard recognizer.state == .began else { return } UIImpactFeedbackGenerator(style: .medium).impactOccurred() onUnlock() } } }