import SwiftUI enum OracleMode: String, CaseIterable { case pipeline = "Pipeline" case accountTimeline = "Account Timeline" case calendarTasks = "Calendar & Tasks" case teamPerformance = "Team Performance" case leadMap = "Lead Map" var icon: String { switch self { case .pipeline: return "square.grid.3x1.below.line.grid.1x2" case .accountTimeline: return "clock.arrow.circlepath" case .calendarTasks: return "calendar" case .teamPerformance: return "person.3" case .leadMap: return "map" } } } struct OracleView: View { @State private var store = AppStore.shared @State private var selectedMode: OracleMode = .pipeline private let refreshTimer = Timer.publish(every: 20, on: .main, in: .common).autoconnect() var body: some View { VStack(alignment: .leading, spacing: 0) { header .padding(.horizontal, 24) .padding(.top, 24) .padding(.bottom, 16) modePicker .padding(.horizontal, 24) .padding(.bottom, 14) if let error = store.errorMessage { errorBanner(error) .padding(.horizontal, 24) .padding(.bottom, 14) } ScrollView { canvas .padding(.horizontal, 24) .padding(.bottom, 24) } } .background(VelocityTheme.background) .task { await store.refresh() } .refreshable { await store.refresh() } .onReceive(refreshTimer) { _ in Task { await store.refresh(silent: true) } } } private var header: some View { HStack { VStack(alignment: .leading, spacing: 3) { Text("Oracle") .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Live sales intelligence assembled from leads, communication events, and calendar data.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() Text(store.lastRefreshAt?.relativeShort ?? "Awaiting sync") .font(.system(size: 11, weight: .medium)) .foregroundStyle(VelocityTheme.mutedFg) } } private var modePicker: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(OracleMode.allCases, id: \.self) { mode in Button { selectedMode = mode } label: { HStack(spacing: 6) { Image(systemName: mode.icon) Text(mode.rawValue) } .font(.system(size: 12, weight: .semibold)) .foregroundStyle(selectedMode == mode ? VelocityTheme.foreground : VelocityTheme.mutedFg) .padding(.horizontal, 14) .padding(.vertical, 10) .background( Capsule() .fill(selectedMode == mode ? VelocityTheme.accent.opacity(0.16) : VelocityTheme.surface) .overlay( Capsule() .stroke(selectedMode == mode ? VelocityTheme.borderAccent : VelocityTheme.borderSubtle, lineWidth: 1) ) ) } .buttonStyle(.plain) } } } } @ViewBuilder private var canvas: some View { switch selectedMode { case .pipeline: pipelineCanvas case .accountTimeline: timelineCanvas case .calendarTasks: calendarCanvas case .teamPerformance: unavailableCanvas( title: "Broker performance feed unavailable", message: "The current mobile contract does not expose broker-attributed performance rollups yet, so Oracle avoids inventing team metrics here." ) case .leadMap: unavailableCanvas( title: "Lead map route unavailable", message: "No production geography route exists for mobile Oracle yet. This view stays disabled until a real geo-backed endpoint is added." ) } } private var pipelineCanvas: some View { let grouped = Dictionary(grouping: store.leads, by: { $0.kanbanStatus.replacingOccurrences(of: "_", with: " ").capitalized }) let stages = grouped.keys.sorted() return VStack(alignment: .leading, spacing: 16) { summaryCard( title: "Pipeline Summary", body: "This view groups live CRM leads by current kanban status. Whale leads and high-score opportunities float to the top of each lane." ) if stages.isEmpty { emptyCard("No live pipeline rows are available yet.") } else { ForEach(stages, id: \.self) { stage in VStack(alignment: .leading, spacing: 12) { Text(stage) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) ForEach((grouped[stage] ?? []).sorted(by: { $0.score > $1.score })) { lead in VStack(alignment: .leading, spacing: 6) { HStack { Text(lead.name) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text("\(lead.score)") .font(.system(size: 12, weight: .bold)) .foregroundStyle(VelocityTheme.accent) } Text("\(lead.qualification.capitalized) · \(lead.unitInterest)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(lead.budget) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.surface) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderSubtle, lineWidth: 1) ) ) } } .padding(18) .glassCard(cornerRadius: 18) } } } } private var timelineCanvas: some View { VStack(alignment: .leading, spacing: 16) { summaryCard( title: "Account Timeline", body: "Recent communication events are pulled from the mobile-edge event stream for the highest-priority leads." ) if store.timelineEvents.isEmpty { emptyCard("No live communication events were returned for the current lead set.") } else { ForEach(store.timelineEvents.prefix(10)) { item in VStack(alignment: .leading, spacing: 8) { HStack { Text(item.leadName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text(item.event.channel.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(VelocityTheme.accent) } Text(item.event.summary ?? "No summary available.") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.foreground) Text(item.event.timestampDate?.relativeShort ?? item.event.timestamp) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) } } } } private var calendarCanvas: some View { VStack(alignment: .leading, spacing: 16) { summaryCard( title: "Calendar & Tasks", body: "Confirmed operator calendar events from the live backend appear here without any synthesized task filler." ) if store.calendarEvents.isEmpty { emptyCard("No live calendar events are scheduled yet for this operator.") } else { ForEach(store.calendarEvents.prefix(10)) { event in VStack(alignment: .leading, spacing: 8) { HStack { Text(event.title) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text(event.status.capitalized) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(color(for: event.status)) } Text(formattedDateRange(event)) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.foreground) Text(event.location ?? "No location") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) } } } } private func unavailableCanvas(title: String, message: String) -> some View { VStack(alignment: .leading, spacing: 16) { summaryCard(title: title, body: message) } } private func summaryCard(title: String, body: String) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(body) .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func emptyCard(_ message: String) -> some View { Text(message) .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func color(for status: String) -> Color { switch status.lowercased() { case "confirmed": return VelocityTheme.success case "tentative": return VelocityTheme.warning default: return VelocityTheme.accent } } private func formattedDateRange(_ event: VelocityCalendarEventDTO) -> String { guard let start = event.startDate else { return event.startAt } let formatter = DateFormatter() formatter.dateFormat = "EEE, MMM d · h:mm a" return formatter.string(from: start) } 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) ) ) } } #Preview { OracleView() }