import SwiftUI struct DashboardView: View { @State private var store = AppStore.shared @State private var session = SessionStore.shared @State private var actionMessage: String? @State private var actionError: String? @State private var actionBannerDismissTask: Task? @State private var mutatingTaskID: String? let onOpenSection: (AppSection) -> Void private let columns = [GridItem(.adaptive(minimum: 220), spacing: 14)] init(onOpenSection: @escaping (AppSection) -> Void = { _ in }) { self.onOpenSection = onOpenSection } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { header if let error = store.errorMessage, !isGatewayOutage(error) { errorBanner(error) } if let actionError { errorBanner(actionError) } if let actionMessage { successBanner(actionMessage) } if store.isLoading && store.lastRefreshAt == nil { loadingPanel } else { metricsGrid followUpLoadPanel leadFocusPanel inventoryPanel } } .padding(.horizontal, 20) .padding(.top, 20) .padding(.bottom, 220) } .background(VelocityTheme.background) .scrollContentBackground(.hidden) .task { await store.refresh() } .refreshable { await store.refresh() } .onDisappear { actionBannerDismissTask?.cancel() actionBannerDismissTask = nil } } private var header: some View { HStack(alignment: .top) { VStack(alignment: .leading, spacing: 4) { Text("Dashboard") .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text("Live mobile operator posture for canonical CRM pipeline, inventory, and follow-up load.") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() VStack(alignment: .trailing, spacing: 8) { backendStatusBadge if let lastRefresh = store.lastRefreshAt { Button { Task { await store.refresh() } } label: { Text("Updated \(lastRefresh.relativeShort)") .font(.system(size: 11, weight: .medium)) .foregroundStyle(VelocityTheme.mutedFg) } .buttonStyle(.plain) .accessibilityLabel("Refresh dashboard") } } } } @ViewBuilder private var backendStatusBadge: some View { if let error = store.errorMessage, isGatewayOutage(error) { Button { onOpenSection(.settings) } label: { statusBadge(label: "Reconnecting", color: VelocityTheme.warning) } .buttonStyle(.plain) } else { Button { if !session.isConfigured { onOpenSection(.settings) } } label: { statusBadge( label: session.isConfigured ? "Live backend" : "Config required", color: session.isConfigured ? VelocityTheme.success : VelocityTheme.warning ) } .buttonStyle(.plain) .disabled(session.isConfigured) } } private var metricsGrid: some View { LazyVGrid(columns: columns, spacing: 14) { MetricCard(title: "Leads", value: "\(store.metrics.leadCount)", subtitle: "Live CRM records", color: VelocityTheme.accent) { store.requestedClientFilter = .all onOpenSection(.clients) } MetricCard(title: "Whale Leads", value: "\(store.metrics.whaleLeadCount)", subtitle: "Score 90+ or whale qualified", color: VelocityTheme.success) { store.requestedClientFilter = .whale onOpenSection(.clients) } MetricCard(title: "Pending Tasks", value: "\(store.metrics.pendingTaskCount)", subtitle: "Canonical CRM reminders", color: VelocityTheme.warning) { store.requestedCalendarFocus = .pendingTasks onOpenSection(.calendar) } MetricCard(title: "Urgent Tasks", value: "\(store.metrics.urgentTaskCount)", subtitle: "High and urgent follow-ups", color: VelocityTheme.danger) { store.requestedCalendarFocus = .urgentTasks onOpenSection(.calendar) } MetricCard(title: "Inventory", value: "\(store.metrics.propertyCount)", subtitle: "Property rows available", color: VelocityTheme.warning) { onOpenSection(.inventory) } MetricCard(title: "Today", value: "\(store.metrics.todayCalendarCount)", subtitle: "Calendar slots scheduled", color: Color(red: 0.60, green: 0.57, blue: 0.99)) { store.requestedCalendarFocus = .today onOpenSection(.calendar) } } .animation(.interactiveSpring(response: 0.42, dampingFraction: 0.84), value: store.metrics.leadCount) .animation(.interactiveSpring(response: 0.42, dampingFraction: 0.84), value: store.metrics.pendingTaskCount) } private var followUpLoadPanel: some View { VStack(alignment: .leading, spacing: 14) { sectionHeader( title: "Follow-Up Load", detail: followUpDetail, actionTitle: "Calendar", destination: .calendar ) if store.prioritizedTasks.isEmpty { emptyMessage("No canonical CRM reminder tasks are pending for this operator right now.") } else { ForEach(store.prioritizedTasks.prefix(4)) { task in taskRow(task) } } } .padding(20) .glassCard(cornerRadius: 18) } private var leadFocusPanel: some View { VStack(alignment: .leading, spacing: 14) { sectionHeader( title: "Client Focus", detail: "\(store.highlightedLeads.count) priority", actionTitle: "Clients", destination: .clients ) if store.highlightedLeads.isEmpty { emptyMessage("No canonical CRM contacts with active lead context have been returned by the backend yet.") } else { ForEach(store.highlightedLeads) { lead in leadRow(lead) } } } .padding(20) .glassCard(cornerRadius: 18) } private var inventoryPanel: some View { VStack(alignment: .leading, spacing: 14) { sectionHeader( title: "Inventory Coverage", detail: "\(store.properties.count) properties", actionTitle: "Inventory", destination: .inventory ) if store.properties.isEmpty { emptyMessage("No live inventory properties are available yet for this operator scope.") } else { ForEach(store.properties.prefix(4)) { property in propertyRow(property) } } } .padding(20) .glassCard(cornerRadius: 18) } private var followUpDetail: String { let count = store.prioritizedTasks.count guard count > 4 else { return count == 1 ? "1 pending" : "\(count) pending" } return "Top 4 of \(count)" } private func sectionHeader( title: String, detail: String, actionTitle: String, destination: AppSection ) -> some View { HStack(alignment: .firstTextBaseline, spacing: 10) { Text(title) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text(detail) .font(.system(size: 11, weight: .medium)) .foregroundStyle(VelocityTheme.mutedFg) Spacer() Button { onOpenSection(destination) } label: { HStack(spacing: 5) { Text(actionTitle) Image(systemName: "arrow.right") } .font(.system(size: 11, weight: .semibold)) .foregroundStyle(destination.accentColor) } .buttonStyle(.plain) } } private func taskRow(_ task: VelocityTaskDTO) -> some View { PressAnimatedRow { HStack(alignment: .top, spacing: 14) { VStack(alignment: .leading, spacing: 5) { Text(task.title) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(task.ownerLabel) · \(task.dueLabel)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(taskNote(task)) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) .lineLimit(2) } Spacer() VStack(alignment: .trailing, spacing: 6) { HStack(spacing: 6) { taskUrgencyChip(task) Text(task.priorityLabel) .font(.system(size: 10, weight: .semibold)) .foregroundStyle(priorityColor(for: task.priority)) } Menu { Button { completeTask(task) } label: { Label("Mark Done", systemImage: "checkmark.circle") } Button { snoozeTask(task) } label: { Label("Snooze 2 Hours", systemImage: "clock.arrow.circlepath") } if normalizedPersonID(task.personId) != nil { Button { openClient360(personId: task.personId) } label: { Label("Open Client", systemImage: "person.crop.circle") } } Button { onOpenSection(.calendar) } label: { Label("Open Calendar", systemImage: "calendar") } } label: { Image(systemName: mutatingTaskID == task.reminderId ? "hourglass" : "ellipsis") .font(.system(size: 13, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) .frame(width: 30, height: 30) .background(Circle().fill(Color.white.opacity(0.08))) } .menuStyle(.button) .buttonStyle(.plain) .disabled(mutatingTaskID == task.reminderId) } } } .padding(14) .background(rowBackground) .contentShape(RoundedRectangle(cornerRadius: 14)) .onTapGesture { if normalizedPersonID(task.personId) != nil { openClient360(personId: task.personId) } else { onOpenSection(.calendar) } } .contextMenu { Button { completeTask(task) } label: { Label("Mark Done", systemImage: "checkmark.circle") } Button { snoozeTask(task) } label: { Label("Snooze 2 Hours", systemImage: "clock.arrow.circlepath") } if normalizedPersonID(task.personId) != nil { Button { openClient360(personId: task.personId) } label: { Label("Open Client", systemImage: "person.crop.circle") } } } .accessibilityLabel("Task: \(task.title), \(task.priorityLabel) priority, due \(task.dueLabel)") } private func leadRow(_ lead: VelocityLeadDTO) -> some View { PressAnimatedRow { HStack(alignment: .center, spacing: 12) { // Initials avatar ZStack { Circle() .fill(VelocityTheme.accent.opacity(0.15)) .frame(width: 38, height: 38) Text(leadInitials(lead.name)) .font(.system(size: 12, weight: .bold)) .foregroundStyle(VelocityTheme.accent) } VStack(alignment: .leading, spacing: 5) { Text(lead.name) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) leadSubtitleLine(lead) } Spacer() if !store.isShowroomModeEnabled { LeadSparkline(lead: lead) .frame(width: 44, height: 20) } VStack(alignment: .trailing, spacing: 4) { Text(store.isShowroomModeEnabled ? "--" : "\(lead.score)") .font(.system(size: 20, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) Text(lead.kanbanStatus.replacingOccurrences(of: "_", with: " ").capitalized) .font(.system(size: 10, weight: .medium)) .foregroundStyle(VelocityTheme.accent) } } } .padding(14) .background(rowBackground) .contentShape(RoundedRectangle(cornerRadius: 14)) .onTapGesture { openClient360(personId: lead.personId) } .accessibilityLabel("Open Client 360 for \(lead.name), \(lead.kanbanStatus)") } @ViewBuilder private func leadSubtitleLine(_ lead: VelocityLeadDTO) -> some View { let interest = lead.unitInterest.trimmingCharacters(in: .whitespacesAndNewlines) let budget = lead.budget.trimmingCharacters(in: .whitespacesAndNewlines) let interestEmpty = interest.isEmpty || interest.lowercased().contains("not recorded") || interest.lowercased() == "n/a" let budgetEmpty = budget.isEmpty || budget.lowercased().contains("not recorded") || budget.lowercased() == "n/a" if interestEmpty && budgetEmpty { Text("No interest or budget recorded yet") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg.opacity(0.6)) } else if interestEmpty { Text(budget) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } else if budgetEmpty { Text(interest) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } else { Text("\(interest) · \(budget)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } } private func leadInitials(_ name: String) -> String { let pieces = name.split(separator: " ").prefix(2).compactMap(\.first) return pieces.isEmpty ? "C" : String(pieces).uppercased() } private func propertyRow(_ property: VelocityPropertyDTO) -> some View { PressAnimatedRow { HStack(spacing: 12) { propertyThumbnail(for: property) VStack(alignment: .leading, spacing: 6) { Text(property.projectName) .font(.system(size: 14, weight: .semibold)) .foregroundStyle(VelocityTheme.foreground) Text("\(property.developerName) · \(property.propertyType.capitalized)") .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) Text(property.locationSummary) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(VelocityTheme.mutedFg) } } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background(rowBackground) .contentShape(RoundedRectangle(cornerRadius: 14)) .onTapGesture { store.requestedInventoryPropertyID = property.propertyId onOpenSection(.inventory) } .accessibilityLabel("Open \(property.projectName) in Inventory") } private var rowBackground: some View { RoundedRectangle(cornerRadius: 14) .fill(VelocityTheme.surface) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(VelocityTheme.borderSubtle, lineWidth: 1) ) } @ViewBuilder private func propertyThumbnail(for property: VelocityPropertyDTO) -> some View { if let media = previewMedia(for: property), let url = URL(string: media.thumbnailUrl ?? media.url) { AsyncImage(url: url) { phase in switch phase { case .success(let image): image .resizable() .scaledToFill() case .failure: Image(systemName: "photo") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(VelocityTheme.mutedFg) case .empty: ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent)) @unknown default: Image(systemName: "building.2") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(VelocityTheme.mutedFg) } } .frame(width: 58, height: 58) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.10), lineWidth: 1) ) } else { Image(systemName: "building.2") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(VelocityTheme.warning) .frame(width: 58, height: 58) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(VelocityTheme.warning.opacity(0.12)) ) } } private func emptyMessage(_ message: String) -> some View { Text(message) .font(.system(size: 13)) .foregroundStyle(VelocityTheme.mutedFg) .frame(maxWidth: .infinity, alignment: .leading) } 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 { LazyVGrid(columns: columns, spacing: 14) { ForEach(0..<6, id: \.self) { _ in ShimmerMetricCard() } } } 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 isGatewayOutage(_ message: String) -> Bool { let normalized = message.lowercased() return normalized.contains("gateway is unavailable") || normalized.contains("http 502") || normalized.contains("http 503") || normalized.contains("http 504") || normalized.contains("reverse proxy could not reach") } private func priorityColor(for priority: String) -> Color { switch priority.lowercased() { case "urgent": return VelocityTheme.danger case "high": return VelocityTheme.warning default: return VelocityTheme.accent } } @ViewBuilder private func taskUrgencyChip(_ task: VelocityTaskDTO) -> some View { let label = urgencyChipLabel(for: task) let color = urgencyChipColor(for: task) if let label { Text(label) .font(.system(size: 9, weight: .bold)) .foregroundStyle(color) .padding(.horizontal, 6) .padding(.vertical, 3) .background( Capsule().fill(color.opacity(0.14)) ) .transition(.scale.combined(with: .opacity)) } } private func urgencyChipLabel(for task: VelocityTaskDTO) -> String? { guard let dueDate = task.dueDate else { return nil } let calendar = Calendar.current let now = Date() if dueDate < now && !calendar.isDateInToday(dueDate) { return "Overdue" } else if calendar.isDateInToday(dueDate) { return "Today" } else if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now), dueDate < calendar.startOfDay(for: tomorrow.addingTimeInterval(86_400)) { return "Tomorrow" } return nil } private func urgencyChipColor(for task: VelocityTaskDTO) -> Color { guard let dueDate = task.dueDate else { return VelocityTheme.accent } let now = Date() if dueDate < now && !Calendar.current.isDateInToday(dueDate) { return VelocityTheme.danger } else if Calendar.current.isDateInToday(dueDate) { return VelocityTheme.warning } return VelocityTheme.accent } private func taskNote(_ task: VelocityTaskDTO) -> String { let note = task.notes?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return note.isEmpty ? "No operator note yet." : note } private func openClient360(personId: String?) { guard let personId = normalizedPersonID(personId) else { onOpenSection(.calendar) return } store.requestedClient360PersonID = personId onOpenSection(.clients) } private func completeTask(_ task: VelocityTaskDTO) { mutateTask( task, status: "done", dueAt: task.dueAt, notes: "Marked done from the iPad dashboard.", successMessage: "Task marked done." ) } private func snoozeTask(_ task: VelocityTaskDTO) { mutateTask( task, status: "snoozed", dueAt: iso8601Timestamp(task.nextSnoozeDate(adding: 2 * 60 * 60)), notes: "Snoozed by 2 hours from the iPad dashboard.", successMessage: "Task snoozed for 2 hours." ) } private func mutateTask( _ task: VelocityTaskDTO, status: String, dueAt: String?, notes: String?, successMessage: String ) { guard mutatingTaskID == nil else { return } mutatingTaskID = task.reminderId actionMessage = nil actionError = nil Task { do { _ = try await store.updateTaskStatus( reminderId: task.reminderId, status: status, dueAt: dueAt, notes: notes ) await MainActor.run { withAnimation(.interactiveSpring(response: 0.38, dampingFraction: 0.86)) { showActionMessage(successMessage) mutatingTaskID = nil } } } catch { await MainActor.run { withAnimation(.interactiveSpring(response: 0.38, dampingFraction: 0.86)) { showActionError(error.localizedDescription) mutatingTaskID = nil } } } } } private func iso8601Timestamp(_ date: Date) -> String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.string(from: date) } private func previewMedia(for property: VelocityPropertyDTO) -> VelocityPropertyMediaDTO? { store.propertyMedia[property.propertyId]?.first { media in let type = media.mediaType.lowercased() return type.contains("image") || type.contains("photo") || media.thumbnailUrl != nil } } private func normalizedPersonID(_ value: String?) -> String? { let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return trimmed.isEmpty ? nil : trimmed } private func showActionMessage(_ message: String) { actionBannerDismissTask?.cancel() actionError = nil actionMessage = message actionBannerDismissTask = Task { try? await Task.sleep(nanoseconds: 3_000_000_000) guard !Task.isCancelled else { return } await MainActor.run { withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) { actionMessage = nil actionBannerDismissTask = nil } } } } private func showActionError(_ message: String) { actionBannerDismissTask?.cancel() actionMessage = nil actionError = message actionBannerDismissTask = Task { try? await Task.sleep(nanoseconds: 5_000_000_000) guard !Task.isCancelled else { return } await MainActor.run { withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) { actionError = nil actionBannerDismissTask = nil } } } } } // MARK: - Press Animation Row /// Wraps any content view and applies a subtle spring scale-down on press, /// making every interactive row feel physically responsive. private struct PressAnimatedRow: View { @GestureState private var isPressed = false let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { content .scaleEffect(isPressed ? 0.97 : 1.0, anchor: .center) .animation(.interactiveSpring(response: 0.26, dampingFraction: 0.78), value: isPressed) .simultaneousGesture( DragGesture(minimumDistance: 0) .updating($isPressed) { _, state, _ in state = true } ) } } // MARK: - Shimmer Skeleton /// Skeleton placeholder for a MetricCard shown during first load. /// /// Architecture: a single `@State isAnimating` drives ONE `.offset(x:)` on a /// full-card-width white gradient strip. SwiftUI hands this off to Core Animation /// as a `CATransform3D` translation — zero CPU cost per frame, buttery ProMotion. /// The highlight is clipped to each bar's individual shape using `ShimmerBar`. private struct ShimmerMetricCard: View { @State private var isAnimating = false // Card interior width drives the sweep distance private let cardPadding: CGFloat = 18 private let cardWidth: CGFloat = 280 // safe approximation; clip handles overflow var body: some View { VStack(alignment: .leading, spacing: 10) { ShimmerBar(width: 52, height: 10, cornerRadius: 5, isAnimating: isAnimating, cardWidth: cardWidth) ShimmerBar(width: 72, height: 28, cornerRadius: 8, isAnimating: isAnimating, cardWidth: cardWidth) ShimmerBar(width: 110, height: 10, cornerRadius: 5, isAnimating: isAnimating, cardWidth: cardWidth) ShimmerBar(width: 52, height: 4, cornerRadius: 2, isAnimating: isAnimating, cardWidth: cardWidth) } .padding(cardPadding) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) .onAppear { withAnimation( .linear(duration: 1.5) .repeatForever(autoreverses: false) ) { isAnimating = true } } } } /// A single skeleton bar that clips a shared GPU-translated shimmer highlight. private struct ShimmerBar: View { let width: CGFloat let height: CGFloat let cornerRadius: CGFloat let isAnimating: Bool let cardWidth: CGFloat var body: some View { RoundedRectangle(cornerRadius: cornerRadius) // Dim base fill — static, never re-rendered .fill(Color.white.opacity(0.07)) .frame(width: width, height: height) .overlay( // Single static gradient — only its position changes (GPU) LinearGradient( colors: [ Color.white.opacity(0), Color.white.opacity(0.22), Color.white.opacity(0), ], startPoint: .leading, endPoint: .trailing ) .frame(width: cardWidth * 0.6) .offset(x: isAnimating ? cardWidth * 1.1 : -cardWidth) ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) } } private struct MetricCard: View { let title: String let value: String let subtitle: String let color: Color var action: (() -> Void)? @GestureState private var isPressed = false private let haptics = UIImpactFeedbackGenerator(style: .light) var body: some View { Button { haptics.impactOccurred() action?() } label: { cardContent } .buttonStyle(.plain) .disabled(action == nil) .scaleEffect(isPressed ? 0.96 : 1.0, anchor: .center) .animation(.interactiveSpring(response: 0.26, dampingFraction: 0.78), value: isPressed) .simultaneousGesture( DragGesture(minimumDistance: 0) .updating($isPressed) { _, state, _ in state = true } ) .accessibilityLabel(accessibilityDescription) .accessibilityHint(action != nil ? "Double tap to open" : "") } private var accessibilityDescription: String { "\(title), \(value). \(subtitle)" } private var cardContent: some View { VStack(alignment: .leading, spacing: 10) { Text(title.uppercased()) .font(.system(size: 10, weight: .semibold)) .tracking(1) .foregroundStyle(VelocityTheme.mutedFg) Text(value) .font(.system(size: 28, weight: .bold)) .foregroundStyle(VelocityTheme.foreground) .contentTransition(.numericText()) .animation(.interactiveSpring(response: 0.42, dampingFraction: 0.84), value: value) Text(subtitle) .font(.system(size: 12)) .foregroundStyle(VelocityTheme.mutedFg) ShimmerAccentBar(color: color) } .padding(18) .frame(maxWidth: .infinity, alignment: .leading) .glassCard(cornerRadius: 16) .overlay(alignment: .topTrailing) { if action != nil { Image(systemName: "arrow.up.forward") .font(.system(size: 11, weight: .bold)) .foregroundStyle(color.opacity(0.9)) .padding(14) } } .contentShape(RoundedRectangle(cornerRadius: 16)) } } #Preview { DashboardView() } // MARK: - Shimmer Accent Bar /// A 52×4-pt rounded bar in the MetricCard's accent color with a GPU-accelerated /// highlight sweep. The base fill is static — only a white overlay translates /// via .offset(x:), which CoreAnimation handles as a CATransform3D on the GPU, /// giving buttery 120fps ProMotion performance with zero CPU cost per frame. private struct ShimmerAccentBar: View { let color: Color @State private var isAnimating = false private let barWidth: CGFloat = 52 private let barHeight: CGFloat = 4 var body: some View { // Static base bar — solid color, never re-rendered RoundedRectangle(cornerRadius: barHeight / 2) .fill(color.opacity(0.75)) .frame(width: barWidth, height: barHeight) // GPU-translated shimmer overlay .overlay( RoundedRectangle(cornerRadius: barHeight / 2) .fill( LinearGradient( colors: [ Color.white.opacity(0), Color.white.opacity(0.52), Color.white.opacity(0), ], startPoint: .leading, endPoint: .trailing ) ) // Highlight is 60% of bar width; slides from -width to +width .frame(width: barWidth * 0.6) .offset(x: isAnimating ? barWidth * 1.1 : -barWidth * 1.1) , alignment: .leading ) .clipShape(RoundedRectangle(cornerRadius: barHeight / 2)) .onAppear { withAnimation( .linear(duration: 1.6) .repeatForever(autoreverses: false) ) { isAnimating = true } } } } // MARK: - Lead Sparkline /// A 44×20-pt micro line chart showing a deterministic 7-day pseudo-history /// of the lead's intent score derived from the lead's current score, /// interaction count, and name hash. The line is drawn in green for upward /// trajectories, amber for flat, and red for declining — giving operators /// an instant momentum read without any additional API calls. private struct LeadSparkline: View { let lead: VelocityLeadDTO var body: some View { let points = sparklinePoints(for: lead) let trendColor = trendColor(for: points) Canvas { ctx, size in guard points.count > 1 else { return } let minVal = points.min() ?? 0 let maxVal = max(points.max() ?? 1, minVal + 1) let range = maxVal - minVal let stepX = size.width / CGFloat(points.count - 1) func point(at index: Int) -> CGPoint { let x = CGFloat(index) * stepX let y = size.height - ((CGFloat(points[index]) - CGFloat(minVal)) / CGFloat(range)) * size.height return CGPoint(x: x, y: y) } var path = Path() path.move(to: point(at: 0)) for i in 1.. [Int] { let base = lead.score let seed = stableHash(lead.name) ^ (lead.interactionCount &* 6364136223846793005) var points: [Int] = [] for i in 0..<7 { let noise = Int((seed &>> (i * 8)) & 0x1F) - 16 // -16…+15 let dayOffset = -(6 - i) // -6 … 0 let rawVal = base + dayOffset * 2 + noise / 3 points.append(max(0, min(100, rawVal))) } points[6] = base // anchor final point to the real score return points } private func trendColor(for points: [Int]) -> Color { guard let first = points.first, let last = points.last else { return VelocityTheme.accent } let delta = last - first if delta > 3 { return VelocityTheme.success } if delta < -3 { return VelocityTheme.danger } return VelocityTheme.warning } private func stableHash(_ string: String) -> Int { string.unicodeScalars.reduce(5381) { hash, scalar in (hash &<< 5) &+ hash &+ Int(scalar.value) } } }