import SwiftUI struct SentinelView: View { private var store: AppStore { AppStore.shared } private let indigo = Color(red: 0.60, green: 0.57, blue: 0.99) var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { pageHeader kpiGrid analyticsRow bottomRow } .padding(24) } .background(VelocityTheme.background) .scrollContentBackground(.hidden) } // MARK: – Sub-views extracted so the type-checker can cope private var pageHeader: some View { VStack(alignment: .leading, spacing: 4) { Text("Sentinel") .font(.system(size: 28, weight: .bold)).foregroundStyle(VelocityTheme.foreground) Text("FaceID · visitor analytics · real-time alerts") .font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg) } } private var kpiGrid: some View { let cols = [GridItem(.adaptive(minimum: 180), spacing: 12)] return LazyVGrid(columns: cols, spacing: 12) { SentinelKPI(icon: "person.2.fill", iconColor: VelocityTheme.accent, label: "Active Visitors", value: "\(store.visitors.count)", sub: "Currently tracked", badge: "LIVE") SentinelKPI(icon: "waveform.path.ecg", iconColor: VelocityTheme.success, label: "Avg Sentiment", value: "\(avgSentiment)%", sub: "Overall mood") SentinelKPI(icon: "eye.fill", iconColor: indigo, label: "Detection Accuracy", value: "\(avgConfidence)%", sub: "Avg confidence") SentinelKPI(icon: "faceid", iconColor: VelocityTheme.warning, label: "Tracked Today", value: "47", sub: "Unique faces") } .animation(.easeInOut(duration: 0.4), value: store.visitors.count) } private var analyticsRow: some View { let cols = [GridItem(.flexible(), spacing: 14), GridItem(.flexible(minimum: 260), spacing: 14)] return LazyVGrid(columns: cols, alignment: .leading, spacing: 14) { ZoneAnalyticsPanel() ClientInsightsPanel() } } private var bottomRow: some View { let cols = [GridItem(.flexible(), spacing: 14), GridItem(.flexible(), spacing: 14), GridItem(.flexible(), spacing: 14)] return LazyVGrid(columns: cols, alignment: .leading, spacing: 14) { SentimentDistributionPanel(visitors: store.visitors) DwellTimePanel() AlertPanel(isActive: store.isAlertActive, message: store.alertMessage) } } private var avgSentiment: Int { guard !store.visitors.isEmpty else { return 0 } let total = store.visitors.reduce(0) { $0 + $1.sentiment.score } return total / store.visitors.count } private var avgConfidence: Int { guard !store.visitors.isEmpty else { return 0 } let total = store.visitors.reduce(0.0) { $0 + $1.confidence } return Int((total / Double(store.visitors.count)) * 100) } } // MARK: – KPI Card private struct SentinelKPI: View { let icon: String; let iconColor: Color let label: String; let value: String; let sub: String var badge: String? = nil var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { ZStack { RoundedRectangle(cornerRadius: 8).fill(iconColor.opacity(0.14)).frame(width: 34, height: 34) Image(systemName: icon).font(.system(size: 14)).foregroundStyle(iconColor) } Spacer() if let badge { HStack(spacing: 4) { Circle().fill(VelocityTheme.success).frame(width: 5, height: 5) Text(badge).font(.system(size: 9, weight: .semibold)).foregroundStyle(VelocityTheme.success) } } } Text(label.uppercased()).font(.system(size: 10, weight: .medium)).tracking(1.0) .foregroundStyle(VelocityTheme.mutedFg) Text(value).font(.system(size: 30, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) .contentTransition(.numericText()) Text(sub).font(.system(size: 11)).foregroundStyle(VelocityTheme.subtleFg) } .padding(18) .frame(maxWidth: .infinity, minHeight: 140, alignment: .topLeading) .background( RoundedRectangle(cornerRadius: 14) .fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(iconColor.opacity(0.18), lineWidth: 1)) ) } } // MARK: – Zone Analytics private struct ZoneAnalyticsPanel: View { private let zones: [(id: String, name: String, count: Int, sentiment: Int)] = [ ("A", "Main Showroom", 5, 72), ("B", "Penthouse Gallery",3, 85), ("C", "Amenity Deck VR", 2, 68), ("D", "Reception", 2, 90), ] var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 6) { Image(systemName: "mappin.and.ellipse").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent) Text("Zone Analytics").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) } ForEach(zones, id: \.id) { zone in HStack(spacing: 10) { ZStack { RoundedRectangle(cornerRadius: 6).fill(VelocityTheme.accent.opacity(0.14)).frame(width: 28, height: 28) Text(zone.id).font(.system(size: 11, weight: .bold)).foregroundStyle(VelocityTheme.accent) } VStack(alignment: .leading, spacing: 2) { Text(zone.name).font(.system(size: 12, weight: .medium)).foregroundStyle(VelocityTheme.foreground) Text("\(zone.count) visitors").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg) } Spacer() HStack(spacing: 4) { let c: Color = zone.sentiment >= 80 ? VelocityTheme.success : zone.sentiment >= 60 ? VelocityTheme.accent : VelocityTheme.warning Circle().fill(c).frame(width: 7, height: 7) Text("\(zone.sentiment)%").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) } } .padding(10) .background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.06), lineWidth: 1))) } } .padding(16) .background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1))) } } // MARK: – Client Insights private struct ClientInsightsPanel: View { private struct Insight { let name: String; let stage: String; let sentiment: String let score: Int; let insight: String; let color: Color var icon: String { score >= 80 ? "arrow.up.right" : score >= 50 ? "minus" : "exclamationmark.triangle" } var scoreColor: Color { score >= 80 ? VelocityTheme.success : score >= 50 ? VelocityTheme.warning : VelocityTheme.danger } } private let insights: [Insight] = [ .init(name: "Quantum Dynamics", stage: "Proposal Review", sentiment: "Positive", score: 92, insight: "Key decision maker showed high engagement. Emphasise custom penthouse integration.", color: VelocityTheme.success), .init(name: "Nebula Ventures", stage: "Discovery", sentiment: "Neutral", score: 45, insight: "Initial interest detected but hesitation around pricing model tier.", color: VelocityTheme.warning), .init(name: "Apex Industries", stage: "Negotiation", sentiment: "Mixed", score: 68, insight: "Confusion markers during contract terms review. Legal team is the blocker.", color: VelocityTheme.danger), .init(name: "Starlight Systems", stage: "Initial Contact", sentiment: "Positive", score: 78, insight: "Strong ESG engagement. Align pitch with sustainability goals to accelerate close.", color: VelocityTheme.accent), ] var body: some View { VStack(alignment: .leading, spacing: 12) { insightHeader insightGrid } .padding(16) .background(RoundedRectangle(cornerRadius: 14) .fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderAccent, lineWidth: 1))) } private var insightHeader: some View { HStack(spacing: 6) { Image(systemName: "sparkles").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent) Text("AI Strategic Insights") .font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) Spacer() Text("LIVE ANALYSIS").font(.system(size: 8, weight: .bold)).tracking(1) .foregroundStyle(VelocityTheme.accent) .padding(.horizontal, 6).padding(.vertical, 3) .background(RoundedRectangle(cornerRadius: 4).fill(VelocityTheme.accent.opacity(0.12)) .overlay(RoundedRectangle(cornerRadius: 4) .stroke(VelocityTheme.accent.opacity(0.2), lineWidth: 1))) } } private var insightGrid: some View { let cols = [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)] return LazyVGrid(columns: cols, alignment: .leading, spacing: 10) { ForEach(insights, id: \.name) { item in InsightCard( name: item.name, stage: item.stage, sentiment: item.sentiment, score: item.score, insight: item.insight, color: item.color, icon: item.icon, scoreColor: item.scoreColor ) } } } } private struct InsightCard: View { struct Item { let name: String; let stage: String; let sentiment: String let score: Int; let insight: String; let color: Color var icon: String { score >= 80 ? "arrow.up.right" : score >= 50 ? "minus" : "exclamationmark.triangle" } var scoreColor: Color { score >= 80 ? VelocityTheme.success : score >= 50 ? VelocityTheme.warning : VelocityTheme.danger } } // Accept ClientInsightsPanel.Insight via protocol duck-typing workaround: let name: String; let stage: String; let sentiment: String let score: Int; let insight: String; let color: Color let icon: String; let scoreColor: Color var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { ZStack { RoundedRectangle(cornerRadius: 6).fill(color.opacity(0.18)).frame(width: 28, height: 28) Image(systemName: icon).font(.system(size: 11)).foregroundStyle(color) } Spacer() Text("\(score)").font(.system(size: 11, weight: .bold)) .foregroundStyle(scoreColor) .padding(.horizontal, 6).padding(.vertical, 2) .background(RoundedRectangle(cornerRadius: 4).fill(scoreColor.opacity(0.15))) } Text(name).font(.system(size: 12, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground).lineLimit(1) Text(insight).font(.system(size: 10)) .foregroundStyle(VelocityTheme.mutedFg).lineLimit(3) HStack { Text(stage).font(.system(size: 9)).foregroundStyle(VelocityTheme.subtleFg) Spacer() Text(sentiment).font(.system(size: 9, weight: .semibold)).foregroundStyle(color) } } .padding(10) .background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(color.opacity(0.15), lineWidth: 1))) } } // MARK: – Sentiment Distribution private struct SentimentDistributionPanel: View { let visitors: [Visitor] var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 6) { Image(systemName: "chart.bar").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent) Text("Sentiment").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) } ForEach(SentimentType.allCases, id: \.self) { type in let count = visitors.filter { $0.sentiment == type }.count let fraction = visitors.isEmpty ? 0.0 : Double(count) / Double(visitors.count) VStack(alignment: .leading, spacing: 4) { HStack { Text(type.emoji).font(.system(size: 14)) Text(type.rawValue.capitalized).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg) Spacer() Text("\(count)").font(.system(size: 11, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.06)).frame(height: 5) RoundedRectangle(cornerRadius: 3).fill(type.color) .frame(width: geo.size.width * fraction, height: 5) .animation(.easeOut(duration: 0.6), value: fraction) } } .frame(height: 5) } } } .padding(16) .background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1))) } } // MARK: – Dwell Time Panel private struct DwellTimePanel: View { private let data: [(range: String, count: Int, trend: String)] = [ ("< 5 min", 3, "down"), ("5–15 min", 5, "up"), ("15–30 min", 8, "up"), ("> 30 min", 4, "stable"), ] var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 6) { Image(systemName: "timer").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent) Text("Dwell Time").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) } let cols = [GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8)] LazyVGrid(columns: cols, spacing: 8) { ForEach(data, id: \.range) { item in VStack(alignment: .leading, spacing: 4) { HStack { Text(item.range).font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg) Spacer() Image(systemName: item.trend == "up" ? "arrow.up.right" : item.trend == "down" ? "arrow.down.right" : "minus") .font(.system(size: 9)) .foregroundStyle(item.trend == "up" ? VelocityTheme.success : item.trend == "down" ? VelocityTheme.danger : VelocityTheme.mutedFg) } Text("\(item.count)").font(.system(size: 22, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) Text("visitors").font(.system(size: 9)).foregroundStyle(VelocityTheme.subtleFg) } .padding(10) .background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.06), lineWidth: 1))) } } } .padding(16) .background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1))) } } // MARK: – Alert Panel private struct AlertPanel: View { let isActive: Bool let message: String var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 6) { Image(systemName: "bell.badge").font(.system(size: 12)).foregroundStyle(VelocityTheme.warning) Text("Alerts").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground) Spacer() Text(isActive ? "Active" : "Clear") .font(.system(size: 9, weight: .semibold)) .foregroundStyle(isActive ? VelocityTheme.warning : VelocityTheme.success) .padding(.horizontal, 7).padding(.vertical, 3) .background(Capsule().fill((isActive ? VelocityTheme.warning : VelocityTheme.success).opacity(0.15)) .overlay(Capsule().stroke((isActive ? VelocityTheme.warning : VelocityTheme.success).opacity(0.25), lineWidth: 1))) } if isActive { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 14)).foregroundStyle(VelocityTheme.warning) Text("Sentiment Alert").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.warning) } Text(message).font(.system(size: 12)).foregroundStyle(VelocityTheme.foreground).lineLimit(3) Text("Just now").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg) } .padding(12) .background(RoundedRectangle(cornerRadius: 10).fill(VelocityTheme.warning.opacity(0.08)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(VelocityTheme.warning.opacity(0.3), lineWidth: 1))) .transition(.asymmetric(insertion: .scale(scale: 0.95).combined(with: .opacity), removal: .scale(scale: 0.95).combined(with: .opacity))) } else { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Image(systemName: "checkmark.shield.fill") .font(.system(size: 14)).foregroundStyle(VelocityTheme.success) Text("All Clear").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.success) } Text("No alerts at this time").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg) Text("System nominal").font(.system(size: 10)).foregroundStyle(VelocityTheme.subtleFg) } .padding(12) .background(RoundedRectangle(cornerRadius: 10).fill(VelocityTheme.success.opacity(0.08)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(VelocityTheme.success.opacity(0.3), lineWidth: 1))) .transition(.asymmetric(insertion: .scale(scale: 0.95).combined(with: .opacity), removal: .scale(scale: 0.95).combined(with: .opacity))) } } .padding(16) .animation(.easeInOut(duration: 0.3), value: isActive) .background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071)) .overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1))) } }