import Combine import SwiftUI enum OracleMode: String, CaseIterable { case pipeline = "Pipeline" case deals = "Deals" 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 .deals: return "target" 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 = OracleModeAvailability.sanitizedProductionSelection(.pipeline) @State private var selectedClient360: VelocityClient360DTO? @State private var selectedClient360PersonID: String? @State private var isClient360Loading = false @State private var client360Error: String? @State private var actionError: String? @State private var actionMessage: String? @State private var activeTaskMutationID: String? @State private var activeLeadMutationID: String? @State private var activeOpportunityMutationID: String? 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) } if let actionError { errorBanner(actionError) .padding(.horizontal, 24) .padding(.bottom, 14) } if let actionMessage { successBanner(actionMessage) .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() } .onAppear { selectedMode = OracleModeAvailability.sanitizedProductionSelection(selectedMode) } .onReceive(refreshTimer) { _ in Task { await store.refresh(silent: true) } } .sheet(isPresented: client360PresentationBinding) { client360Sheet } } 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 canonical CRM, 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(OracleModeAvailability.productionVisibleModes, 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 OracleModeAvailability.sanitizedProductionSelection(selectedMode) { case .pipeline: pipelineCanvas case .deals: dealsCanvas case .accountTimeline: timelineCanvas case .calendarTasks: calendarCanvas case .teamPerformance, .leadMap: unavailableCanvas( title: "Oracle mode unavailable", message: "This Oracle mode is intentionally hidden from the production iPad scope until a real backend contract exists." ) } } private var pipelineCanvas: some View { return VStack(alignment: .leading, spacing: 16) { productionScopeNote summaryCard( title: "Pipeline Summary", body: "This view reads the canonical `/api/crm/kanban` board directly, so the iPad pipeline reflects the real backend stage model and lane counts." ) if store.kanbanColumns.allSatisfy({ $0.count == 0 }) { emptyCard("No live canonical pipeline rows are available yet.") } else { ForEach(store.kanbanColumns) { column in VStack(alignment: .leading, spacing: 12) { HStack { Text(column.label) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() Text("\(column.count)") .font(.system(size: 11, weight: .semibold)) .foregroundStyle(VelocityTheme.accent) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() .fill(VelocityTheme.accent.opacity(0.12)) .overlay( Capsule() .stroke(VelocityTheme.borderAccent, lineWidth: 1) ) ) } if column.items.isEmpty { Text("No canonical leads are currently in this lane.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } ForEach(column.items) { lead in HStack(alignment: .top, spacing: 12) { Button { openClient360(for: lead.personId) } label: { VStack(alignment: .leading, spacing: 6) { Text(lead.clientName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(lead.buyerTypeLabel) · \(lead.budgetSummary)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text("\(lead.urgencyLabel) urgency · \(lead.clientPhone ?? "No phone")") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) VStack(alignment: .trailing, spacing: 8) { Text("\(lead.displayIntentScore)") .font(.system(size: 12, weight: .bold)) .foregroundStyle(VelocityTheme.accent) if activeLeadMutationID == lead.leadId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { leadStageMenu( leadId: lead.leadId, personId: lead.personId, currentStatus: column.status ) } } } .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) { productionScopeNote 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 Button { openClient360( personId: store.leads.first(where: { $0.id == item.leadId })?.personId, missingContextMessage: "This timeline event is missing a client person_id, so Client 360 cannot be opened." ) } label: { 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) } .buttonStyle(.plain) } } } } private var dealsCanvas: some View { VStack(alignment: .leading, spacing: 16) { productionScopeNote summaryCard( title: "Deals", body: "This view reads canonical `/api/crm/opportunities` directly so the iPad can verify real opportunity rows from the backend." ) if store.opportunities.isEmpty { emptyCard("No live canonical opportunities are available yet for this operator scope.") } else { ForEach(store.opportunities.prefix(20)) { opportunity in HStack(alignment: .top, spacing: 12) { Button { openClient360( personId: opportunity.personId, missingContextMessage: "This opportunity is missing a client person_id, so Client 360 cannot be opened." ) } label: { VStack(alignment: .leading, spacing: 8) { Text(opportunity.clientName ?? "Canonical opportunity") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(opportunity.formattedValue) · \(opportunity.projectName ?? "Project pending")") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.foreground) Text("\(opportunity.nextAction ?? "Next action pending") · \(opportunity.clientPhone ?? "No phone")") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(opportunity.probabilityLabel) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) VStack(alignment: .trailing, spacing: 8) { Text(opportunity.stage.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(VelocityTheme.accent) if activeOpportunityMutationID == opportunity.opportunityId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { opportunityActionsMenu(opportunity) } } } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) } } } } private var calendarCanvas: some View { VStack(alignment: .leading, spacing: 16) { productionScopeNote summaryCard( title: "Calendar & Tasks", body: "Confirmed operator calendar events and canonical CRM reminder tasks are shown here without synthetic filler." ) if store.calendarEvents.isEmpty && store.prioritizedTasks.isEmpty { emptyCard("No live calendar events or canonical CRM tasks are scheduled yet for this operator.") } else { if !store.calendarEvents.isEmpty { VStack(alignment: .leading, spacing: 12) { Text("Calendar Events") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) 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) } } } if !store.prioritizedTasks.isEmpty { VStack(alignment: .leading, spacing: 12) { Text("Pending Follow-Up Tasks") .font(.system(size: 15, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) ForEach(store.prioritizedTasks.prefix(8)) { task in HStack(alignment: .top, spacing: 12) { Button { openClient360( personId: task.personId, missingContextMessage: "This task is missing a client person_id, so Client 360 cannot be opened." ) } label: { VStack(alignment: .leading, spacing: 8) { Text(task.title) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(task.dueLabel) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.foreground) Text("\(task.ownerLabel) · \(task.clientPhone ?? "No phone")") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(taskNote(task)) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) .lineLimit(2) } .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) VStack(alignment: .trailing, spacing: 8) { Text(task.priorityLabel) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(taskColor(for: task.priority)) if activeTaskMutationID == task.reminderId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { taskActionsMenu(task) } } } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) } } } } } } private var productionScopeNote: some View { VStack(alignment: .leading, spacing: 8) { Text("Mobile Oracle Scope") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("This production iPad build shows only live-backed Oracle views. Team Performance and Lead Map stay hidden until the backend exposes real mobile contracts.") .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 taskColor(for priority: String) -> Color { switch priority.lowercased() { case "urgent": return VelocityTheme.danger case "high": return VelocityTheme.warning default: return VelocityTheme.accent } } private func taskNote(_ task: VelocityTaskDTO) -> String { let note = task.notes?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return note.isEmpty ? "No reminder note provided." : note } 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 opportunityActionsMenu(_ opportunity: VelocityOpportunityDTO) -> some View { Menu { Menu("Move Stage") { ForEach(canonicalOpportunityStages.filter { $0 != opportunity.stage.lowercased() }, id: \.self) { stage in Button { mutateOpportunity( opportunity, stage: stage, probability: nil, nextAction: opportunity.nextAction, notes: "Moved from the iPad Oracle deal workspace." ) } label: { Text(stageLabel(stage)) } } } Menu("Set Probability") { ForEach([25, 50, 75, 90], id: \.self) { probability in Button { mutateOpportunity( opportunity, stage: nil, probability: probability, nextAction: opportunity.nextAction, notes: "Probability updated from the iPad Oracle deal workspace." ) } label: { Text("\(probability)%") } } } Button { mutateOpportunity( opportunity, stage: nil, probability: nil, nextAction: "Schedule commercial follow-up", notes: "Next action updated from the iPad Oracle deal workspace." ) } label: { Label("Set Follow-Up Action", systemImage: "phone.arrow.up.right") } Button { mutateOpportunity( opportunity, stage: "closed_won", probability: 100, nextAction: "Complete booking documentation", notes: "Marked closed won from the iPad Oracle deal workspace." ) } label: { Label("Close Won", systemImage: "checkmark.seal") } Button(role: .destructive) { mutateOpportunity( opportunity, stage: "closed_lost", probability: 0, nextAction: "Capture loss reason", notes: "Marked closed lost from the iPad Oracle deal workspace." ) } label: { Label("Close Lost", systemImage: "xmark.seal") } } label: { menuIcon("ellipsis.circle") } } private func taskActionsMenu(_ task: VelocityTaskDTO) -> some View { Menu { if task.status.lowercased() != "done" { Button { mutateTask(task, status: "done", dueAt: nil, notes: "Marked done from the iPad Oracle workspace.") } label: { Label("Mark Done", systemImage: "checkmark.circle") } } if task.status.lowercased() != "pending" { Button { mutateTask(task, status: "pending", dueAt: task.dueAt, notes: "Reopened from the iPad Oracle workspace.") } label: { Label("Reopen", systemImage: "arrow.counterclockwise") } } if !["done", "cancelled"].contains(task.status.lowercased()) { Button { let snoozeDate = task.nextSnoozeDate(adding: 2 * 60 * 60) mutateTask( task, status: "snoozed", dueAt: iso8601Timestamp(snoozeDate), notes: "Snoozed by 2 hours from the iPad Oracle workspace." ) } label: { Label("Snooze 2h", systemImage: "clock.badge") } Button { let snoozeDate = task.nextSnoozeDate(adding: 24 * 60 * 60) mutateTask( task, status: "snoozed", dueAt: iso8601Timestamp(snoozeDate), notes: "Snoozed by 1 day from the iPad Oracle workspace." ) } label: { Label("Snooze 1 Day", systemImage: "moon") } } if task.status.lowercased() != "cancelled" { Button(role: .destructive) { mutateTask(task, status: "cancelled", dueAt: nil, notes: "Cancelled from the iPad Oracle workspace.") } label: { Label("Cancel Task", systemImage: "xmark.circle") } } } label: { menuIcon("ellipsis.circle") } } private func leadStageMenu( leadId: String, personId: String, currentStatus: String ) -> some View { Menu { ForEach(canonicalLeadStages.filter { $0 != currentStatus.lowercased() }, id: \.self) { status in Button { mutateLeadStage( leadId: leadId, personId: personId, status: status, notes: "Moved from the iPad Oracle pipeline." ) } label: { Text(stageLabel(status)) } } } label: { menuIcon("arrow.triangle.swap") } } private func mutateTask( _ task: VelocityTaskDTO, status: String, dueAt: String?, notes: String ) { actionError = nil actionMessage = nil activeTaskMutationID = task.reminderId Task { do { _ = try await store.updateTaskStatus( reminderId: task.reminderId, status: status, dueAt: dueAt, notes: notes ) await refreshClient360IfNeeded(for: task.personId ?? selectedClient360PersonID) await MainActor.run { activeTaskMutationID = nil actionMessage = taskActionMessage(status: status) } } catch { await MainActor.run { activeTaskMutationID = nil actionError = error.localizedDescription } } } } private func mutateLeadStage( leadId: String, personId: String, status: String, notes: String ) { actionError = nil actionMessage = nil activeLeadMutationID = leadId Task { do { _ = try await store.updateLeadStage( leadId: leadId, status: status, notes: notes ) await refreshClient360IfNeeded(for: personId) await MainActor.run { activeLeadMutationID = nil actionMessage = "Lead moved to \(stageLabel(status))." } } catch { await MainActor.run { activeLeadMutationID = nil actionError = error.localizedDescription } } } } private func mutateOpportunity( _ opportunity: VelocityOpportunityDTO, stage: String?, probability: Int?, nextAction: String?, notes: String ) { actionError = nil actionMessage = nil activeOpportunityMutationID = opportunity.opportunityId Task { do { _ = try await store.updateOpportunity( opportunityId: opportunity.opportunityId, stage: stage, probability: probability, nextAction: nextAction, notes: notes ) await refreshClient360IfNeeded(for: opportunity.personId ?? selectedClient360PersonID) await MainActor.run { activeOpportunityMutationID = nil actionMessage = opportunityActionMessage(stage: stage, probability: probability, nextAction: nextAction) } } catch { await MainActor.run { activeOpportunityMutationID = nil actionError = error.localizedDescription } } } } private func refreshClient360IfNeeded(for personId: String?) async { guard let personId, selectedClient360PersonID == personId else { return } do { let snapshot = try await VelocityAPIClient.shared.fetchClient360(personId: personId) await MainActor.run { selectedClient360 = snapshot client360Error = nil } } catch { await MainActor.run { client360Error = error.localizedDescription } } } private var canonicalLeadStages: [String] { [ "new", "contacted", "qualified", "site_visit_scheduled", "site_visited", "negotiation", "booking_initiated", "booked", "lost", "dormant", ] } private var canonicalOpportunityStages: [String] { [ "prospect", "qualified", "proposal", "site_visit", "negotiation", "booking", "agreement", "closed_won", "closed_lost", ] } private func stageLabel(_ status: String) -> String { status.replacingOccurrences(of: "_", with: " ").capitalized } private func iso8601Timestamp(_ date: Date) -> String { ISO8601DateFormatter().string(from: date) } private var client360PresentationBinding: Binding { Binding( get: { selectedClient360PersonID != nil }, set: { isPresented in if !isPresented { selectedClient360PersonID = nil selectedClient360 = nil client360Error = nil actionMessage = nil isClient360Loading = false } } ) } @ViewBuilder private var client360Sheet: some View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 18) { if isClient360Loading { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 40) } else { if let client360Error { errorBanner(client360Error) } if let actionError { errorBanner(actionError) } if let actionMessage { successBanner(actionMessage) } if let snapshot = selectedClient360 { client360IdentityCard(snapshot) client360LeadCard(snapshot) client360OpportunitiesCard(snapshot) client360TasksCard(snapshot) client360InteractionsCard(snapshot) client360SignalsCard(snapshot) } } } .padding(20) } .background(VelocityTheme.background) .navigationTitle("Client 360") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { selectedClient360PersonID = nil selectedClient360 = nil client360Error = nil actionMessage = nil isClient360Loading = false } } } } } private func openClient360(for personId: String) { selectedClient360PersonID = personId selectedClient360 = nil client360Error = nil actionError = nil actionMessage = nil isClient360Loading = true Task { do { let snapshot = try await VelocityAPIClient.shared.fetchClient360(personId: personId) await MainActor.run { selectedClient360 = snapshot client360Error = nil isClient360Loading = false } } catch { await MainActor.run { selectedClient360 = nil client360Error = error.localizedDescription isClient360Loading = false } } } } private func openClient360(personId: String?, missingContextMessage: String) { guard let personId, !personId.isEmpty else { actionError = missingContextMessage actionMessage = nil return } openClient360(for: personId) } private func client360IdentityCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { Text(snapshot.identity.fullName) .font(.system(size: 24, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("\(snapshot.identity.buyerType.map { $0.replacingOccurrences(of: "_", with: " ").capitalized } ?? "CRM contact") · \(snapshot.identity.primaryPhone ?? "No phone")") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) if let email = snapshot.identity.primaryEmail { Text(email) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } if !snapshot.identity.personaLabels.isEmpty { Text(snapshot.identity.personaLabels.joined(separator: " · ")) .font(.system(size: 12, weight: .medium)) .foregroundStyle(VelocityTheme.accent) } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func client360LeadCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { HStack { Text("Lead Posture") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Spacer() if let lead = snapshot.currentLead { if activeLeadMutationID == lead.leadId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { leadStageMenu( leadId: lead.leadId, personId: snapshot.identity.personId, currentStatus: lead.status ) } } } if let lead = snapshot.currentLead { Text("\(lead.status.replacingOccurrences(of: "_", with: " ").capitalized) · \(lead.urgency?.capitalized ?? "Normal") urgency") .font(.system(size: 13)) .foregroundStyle(VelocityTheme.foreground) Text(lead.budgetBand ?? "Budget pending") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) if !lead.motivations.isEmpty { Text("Motivations: \(lead.motivations.joined(separator: ", "))") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } if !lead.objections.isEmpty { Text("Objections: \(lead.objections.joined(separator: ", "))") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } } else { Text("No active canonical lead context was returned for this client.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func client360OpportunitiesCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Active Opportunities") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) if snapshot.activeOpportunities.isEmpty { Text("No active opportunities are currently attached to this client.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } else { ForEach(snapshot.activeOpportunities) { opportunity in HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 6) { Text(opportunity.stage.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(opportunity.formattedValue) · \(opportunity.nextAction ?? "Next action pending")") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(opportunity.probabilityLabel) .font(.system(size: 11)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() if activeOpportunityMutationID == opportunity.opportunityId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { opportunityActionsMenu(opportunity) } } .padding(.vertical, 4) } } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func client360TasksCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Follow-Up Tasks") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) if snapshot.tasks.isEmpty { Text("No pending or snoozed tasks were returned for this client.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } else { ForEach(snapshot.tasks) { task in HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 6) { Text(task.title) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(task.priorityLabel) · \(task.dueLabel)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() if activeTaskMutationID == task.reminderId { ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) } else { taskActionsMenu(task) } } .padding(.vertical, 4) } } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func client360InteractionsCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Recent Interactions") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) if snapshot.recentInteractions.isEmpty { Text("No recent canonical interactions were returned for this client.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } else { ForEach(snapshot.recentInteractions.prefix(5)) { interaction in VStack(alignment: .leading, spacing: 6) { Text(interaction.channel.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(interaction.summary ?? interaction.interactionType.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } .padding(.vertical, 4) } } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } private func client360SignalsCard(_ snapshot: VelocityClient360DTO) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Signals And Actions") .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) if let score = snapshot.primaryQDScore { Text("\(score.scoreType.replacingOccurrences(of: "_", with: " ").capitalized) score: \(score.displayScore)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.foreground) } if !snapshot.riskFlags.isEmpty { Text("Risk flags: \(snapshot.riskFlags.joined(separator: ", "))") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } if !snapshot.recommendedNextActions.isEmpty { Text("Next actions: \(snapshot.recommendedNextActions.joined(separator: " · "))") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } if !snapshot.propertyInterests.isEmpty { Text("Primary interest: \(snapshot.propertyInterests[0].projectName)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 18) } 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 successBanner(_ message: String) -> some View { Text(message) .font(.system(size: 13, weight: .medium)) .foregroundStyle(VelocityTheme.success) .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.success.opacity(0.10)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.success.opacity(0.22), lineWidth: 1) ) ) } private func taskActionMessage(status: String) -> String { switch status { case "done": return "Task marked done." case "pending": return "Task reopened." case "snoozed": return "Task snoozed." case "cancelled": return "Task cancelled." default: return "Task updated." } } private func opportunityActionMessage(stage: String?, probability: Int?, nextAction: String?) -> String { if let stage { return "Opportunity moved to \(stageLabel(stage))." } if let probability { return "Opportunity probability set to \(probability)%." } if nextAction != nil { return "Opportunity follow-up action updated." } return "Opportunity updated." } private func menuIcon(_ systemName: String) -> some View { Image(systemName: systemName) .font(.system(size: 17, weight: .semibold)) .foregroundStyle(VelocityTheme.mutedFg) .frame(width: 44, height: 44) .contentShape(Rectangle()) } } #Preview { OracleView() }