import SwiftUI private struct CommunicationThread: Identifiable { let id: String let leadName: String let channel: String let status: String let summary: String let nextAction: String let updatedAt: String let accent: Color } private struct CommunicationAlert: Identifiable { let id: String let title: String let detail: String let severity: String let color: Color } struct CommunicationsView: View { @State private var selectedThread: String? @State private var threads: [CommunicationThread] = [] @State private var alerts: [CommunicationAlert] = [] @State private var isLoading = true @State private var errorMessage: String? private let refreshTimer = Timer.publish(every: 15, on: .main, in: .common).autoconnect() private var activeThread: CommunicationThread? { threads.first(where: { $0.id == selectedThread }) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { header if let errorMessage { errorBanner(errorMessage) } if isLoading { loadingPanel } else { alertsStrip HStack(alignment: .top, spacing: 18) { threadRail detailPanel } } } .padding(20) } .background(VelocityTheme.background) .scrollContentBackground(.hidden) .task { await loadLiveData() } .refreshable { await loadLiveData() } .onReceive(refreshTimer) { _ in Task { await loadLiveData(silent: true) } } } private var header: some View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) { Text("Communications") .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Phone, WhatsApp, transcript, and memory edge across active leads.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() VStack(alignment: .trailing, spacing: 8) { statusBadge(label: "\(threads.count) live threads", color: VelocityTheme.success) if let queueAlert = alerts.first(where: { $0.id == "pending_transcriptions" }) { statusBadge(label: queueAlert.detail, color: queueAlert.color) } } } } private var alertsStrip: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(alerts) { alert in VStack(alignment: .leading, spacing: 8) { HStack { Text(alert.severity) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(alert.color) Spacer() Circle() .fill(alert.color) .frame(width: 8, height: 8) } Text(alert.title) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(alert.detail) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) .lineLimit(3) } .padding(16) .frame(width: 250, alignment: .leading) .glassCard(cornerRadius: 16) } } } } private var threadRail: some View { VStack(alignment: .leading, spacing: 12) { Text("Active Threads") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) if threads.isEmpty { detailRow(title: "Live data", value: "No communication events have been captured for current leads yet.") } ForEach(threads) { thread in Button { selectedThread = thread.id } label: { VStack(alignment: .leading, spacing: 10) { HStack { VStack(alignment: .leading, spacing: 2) { Text(thread.leadName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(thread.channel) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() Text(thread.updatedAt) .font(.system(size: 10, weight: .medium)) .foregroundStyle(VelocityTheme.mutedFg) } Text(thread.summary) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) .lineLimit(3) HStack { Text(thread.status.uppercased()) .font(.system(size: 9, weight: .semibold)) .tracking(1) .foregroundStyle(thread.accent) Spacer() Text(thread.nextAction) .font(.system(size: 10, weight: .medium)) .foregroundStyle(VelocityTheme.foreground) } } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 16) .fill(selectedThread == thread.id ? thread.accent.opacity(0.12) : VelocityTheme.surface) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(selectedThread == thread.id ? thread.accent.opacity(0.25) : VelocityTheme.borderSubtle, lineWidth: 1) ) ) } .buttonStyle(.plain) } } .padding(18) .frame(maxWidth: 360, alignment: .topLeading) .glassCard(cornerRadius: 20) } private var detailPanel: some View { VStack(alignment: .leading, spacing: 16) { HStack { VStack(alignment: .leading, spacing: 4) { Text(activeThread?.leadName ?? "Select a thread") .font(.system(size: 22, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text(activeThread?.channel ?? "Communication detail") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() if let thread = activeThread { statusBadge(label: thread.status, color: thread.accent) } } VStack(alignment: .leading, spacing: 12) { detailRow(title: "Latest summary", value: activeThread?.summary ?? "No thread selected") detailRow(title: "Next operator action", value: activeThread?.nextAction ?? "None") detailRow(title: "Memory extraction", value: activeThread != nil ? "Backed by persisted mobile-edge communication events and live backend alerts." : "No communication memory available.") detailRow(title: "Suggested response", value: activeThread != nil ? "Use the current thread state, transcript queue, and calendar urgency to choose the next operator action." : "Select a thread to view live context.") } VStack(alignment: .leading, spacing: 12) { Text("Recent activity") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) ForEach(alerts.prefix(3)) { alert in activityCard(icon: alertIcon(for: alert.id), title: alert.title, detail: alert.detail) } } } .padding(22) .frame(maxWidth: .infinity, alignment: .topLeading) .glassCard(cornerRadius: 20) } private func detailRow(title: String, value: String) -> some View { VStack(alignment: .leading, spacing: 6) { Text(title.uppercased()) .font(.system(size: 10, weight: .semibold)) .tracking(1) .foregroundStyle(VelocityTheme.mutedFg) Text(value) .font(.system(size: 14)) .foregroundStyle(VelocityTheme.foreground) } } private func activityCard(icon: String, title: String, detail: String) -> some View { HStack(alignment: .top, spacing: 12) { ZStack { RoundedRectangle(cornerRadius: 10) .fill(VelocityTheme.accent.opacity(0.14)) .frame(width: 38, height: 38) Image(systemName: icon) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(VelocityTheme.accent) } VStack(alignment: .leading, spacing: 4) { Text(title) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(detail) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() } .padding(14) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.surface) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderSubtle, lineWidth: 1) ) ) } private func statusBadge(label: String, color: Color) -> some View { Text(label) .font(.system(size: 11, weight: .semibold)) .foregroundStyle(color) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() .fill(color.opacity(0.12)) .overlay( Capsule() .stroke(color.opacity(0.22), lineWidth: 1) ) ) } private var loadingPanel: some View { VStack(alignment: .leading, spacing: 12) { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) Text("Loading live communications...") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("Velocity is fetching current leads, communication events, and alert state from the backend.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(20) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 20) } private func errorBanner(_ message: String) -> some View { Text(message) .font(.system(size: 13, weight: .medium)) .foregroundStyle(VelocityTheme.danger) .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.danger.opacity(0.10)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.danger.opacity(0.22), lineWidth: 1) ) ) } private func loadLiveData(silent: Bool = false) async { if !silent { isLoading = true } do { async let leadsTask = VelocityAPIClient.shared.fetchLeads() async let alertsTask = VelocityAPIClient.shared.fetchAlerts() let leads = try await leadsTask let alertSnapshot = try await alertsTask let topLeads = Array(leads.sorted(by: { $0.score > $1.score }).prefix(8)) var fetchedThreads: [CommunicationThread] = [] for lead in topLeads { let events = try await VelocityAPIClient.shared.fetchEvents(for: lead.id, limit: 1) let latest = events.first fetchedThreads.append( CommunicationThread( id: lead.id, leadName: lead.name, channel: latest.map { channelLabel($0.channel) } ?? sourceLabel(lead.source), status: statusLabel(for: lead, event: latest), summary: latest?.summary ?? "No communication events captured yet for this lead.", nextAction: nextActionLabel(for: lead, event: latest), updatedAt: latest.map { relativeShort($0.timestamp) } ?? "No events", accent: accentColor(for: lead, event: latest) ) ) } let fetchedAlerts = buildAlerts(from: alertSnapshot) await MainActor.run { threads = fetchedThreads alerts = fetchedAlerts if selectedThread == nil { selectedThread = fetchedThreads.first?.id } errorMessage = nil isLoading = false } } catch { await MainActor.run { errorMessage = error.localizedDescription threads = [] alerts = [] isLoading = false } } } private func buildAlerts(from snapshot: VelocityAlertSnapshotDTO) -> [CommunicationAlert] { [ CommunicationAlert( id: "pending_insights", title: "Pending insights", detail: "\(snapshot.pendingInsights) insight recommendations need operator review.", severity: "Priority", color: VelocityTheme.danger ), CommunicationAlert( id: "pending_transcriptions", title: "Transcription queue", detail: "\(snapshot.pendingTranscriptions) transcript jobs waiting.", severity: "Queue", color: VelocityTheme.warning ), CommunicationAlert( id: "calendar_due", title: "Calendar due soon", detail: "\(snapshot.upcomingCalendarEvents24h) calendar events are due in the next 24 hours.", severity: "Calendar", color: VelocityTheme.success ), ] } private func statusLabel(for lead: VelocityLeadDTO, event: VelocityCommunicationEventDTO?) -> String { if event == nil { return "No events yet" } if lead.score >= 90 { return "Whale priority" } return lead.kanbanStatus } private func nextActionLabel(for lead: VelocityLeadDTO, event: VelocityCommunicationEventDTO?) -> String { if event?.recordingRef != nil { return "Review transcript" } if lead.score >= 90 { return "Schedule follow-up" } return "Update operator note" } private func accentColor(for lead: VelocityLeadDTO, event: VelocityCommunicationEventDTO?) -> Color { if event?.recordingRef != nil { return VelocityTheme.accent } if lead.score >= 90 { return VelocityTheme.success } return VelocityTheme.warning } private func alertIcon(for id: String) -> String { switch id { case "pending_transcriptions": return "waveform.badge.mic" case "calendar_due": return "calendar.badge.plus" default: return "brain.head.profile" } } private func channelLabel(_ value: String) -> String { value.replacingOccurrences(of: "_", with: " ").capitalized } private func sourceLabel(_ value: String) -> String { value.replacingOccurrences(of: "_", with: " ").capitalized } private func relativeShort(_ iso: String) -> String { let formatter = ISO8601DateFormatter() guard let date = formatter.date(from: iso) else { return iso } let delta = Int(Date().timeIntervalSince(date)) if delta < 60 { return "now" } if delta < 3600 { return "\(delta / 60)m ago" } if delta < 86400 { return "\(delta / 3600)h ago" } return "\(delta / 86400)d ago" } } #Preview { CommunicationsView() }