I have attached the screenshots of the native SwiftUI app. <img width="1705" alt="image.png" src="attachments/59fec2f3-0ae2-4b58-9349-457618ea0678"> <img width="1699" alt="image.png" src="attachments/0bf7c4f9-c883-4929-be36-774685b82fc4"> <img width="1698" alt="image.png" src="attachments/e3407e84-aaf2-45c0-9325-247d4020bace"> <img width="1694" alt="image.png" src="attachments/ee2cd47d-800d-4a40-855c-d54856680e79"> <img width="1694" alt="image.png" src="attachments/a2c902f1-9bc9-4427-8cae-b5801527c1ff"> Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #2 Co-authored-by: sayan <sayan@desineuron.in> Co-committed-by: sayan <sayan@desineuron.in>
961 lines
44 KiB
Swift
961 lines
44 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: – Oracle Canvas Modes
|
||
enum OracleMode: String, CaseIterable {
|
||
case pipeline = "Pipeline"
|
||
case teamPerformance = "Team Performance"
|
||
case accountTimeline = "Account Timeline"
|
||
case leadMap = "Lead Map"
|
||
case calendarTasks = "Calendar & Tasks"
|
||
|
||
var icon: String {
|
||
switch self {
|
||
case .pipeline: return "square.grid.3x1.below.line.grid.1x2"
|
||
case .teamPerformance: return "person.3"
|
||
case .accountTimeline: return "clock.arrow.circlepath"
|
||
case .leadMap: return "map"
|
||
case .calendarTasks: return "calendar"
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Pipeline mock data (extended with detail fields)
|
||
struct OracleLeadCard: Identifiable {
|
||
let id = UUID()
|
||
let initials: String
|
||
let name: String
|
||
let company: String
|
||
let value: String
|
||
let status: LeadStatus
|
||
let phone: String
|
||
let interest: String
|
||
let qualification: String
|
||
}
|
||
|
||
private let pipelineData: [(stage: String, cards: [OracleLeadCard])] = [
|
||
("New", [
|
||
OracleLeadCard(initials: "JW", name: "James Wilson", company: "Website", value: "AED 3.5M", status: .new,
|
||
phone: "+971 52 456 7890", interest: "1BR Investment", qualification: "Potential"),
|
||
]),
|
||
("Qualified", [
|
||
OracleLeadCard(initials: "FH", name: "Fatima Hassan", company: "WhatsApp", value: "AED 12M", status: .qualified,
|
||
phone: "+971 54 321 0987", interest: "3BR + Maid", qualification: "Whale"),
|
||
OracleLeadCard(initials: "SC", name: "Sarah Chen", company: "Walk-in", value: "AED 6.5M", status: .engaged,
|
||
phone: "+971 50 987 6543", interest: "2BR Sea View", qualification: "Potential"),
|
||
]),
|
||
("Proposal", [
|
||
OracleLeadCard(initials: "MA", name: "Mohammed Al-Rashid", company: "WhatsApp", value: "AED 15M+", status: .hot,
|
||
phone: "+971 55 123 4567", interest: "Penthouse Suite", qualification: "Whale"),
|
||
]),
|
||
("Closed", [
|
||
OracleLeadCard(initials: "DK", name: "David Kumar", company: "Walk-in", value: "AED 20M", status: .closed,
|
||
phone: "+971 56 789 0123", interest: "Full Floor", qualification: "Whale"),
|
||
]),
|
||
]
|
||
|
||
struct TeamMemberData: Identifiable {
|
||
let id = UUID()
|
||
let initials: String; let name: String; let deals: Int; let revenue: String; let trend: String
|
||
}
|
||
private let teamData: [TeamMemberData] = [
|
||
.init(initials: "RA", name: "Rania Al-Farsi", deals: 42, revenue: "$2.1M", trend: "↑ 18%"),
|
||
.init(initials: "KM", name: "Khaled Mensah", deals: 31, revenue: "$1.6M", trend: "↑ 12%"),
|
||
.init(initials: "LT", name: "Lina Torres", deals: 28, revenue: "$1.3M", trend: "→ 2%"),
|
||
.init(initials: "AH", name: "Ahmed Hassan", deals: 19, revenue: "$0.9M", trend: "↓ 5%"),
|
||
]
|
||
|
||
struct OracleTimelineEvent: Identifiable {
|
||
let id = UUID()
|
||
let badge: String; let summary: String; let when: String; let detail: String
|
||
}
|
||
private let timelineEvents: [OracleTimelineEvent] = [
|
||
.init(badge: "MEETING", summary: "VR Amenity Tour – Apex Innovations", when: "2h ago",
|
||
detail: "CFO and Legal Director attended. Strong reaction to the panoramic sea view suite. Follow-up proposal requested by Tuesday."),
|
||
.init(badge: "EMAIL", summary: "Proposal deck sent to legal team", when: "Yesterday",
|
||
detail: "58-page proposal + payment plan schedule sent via DocuSign. Legal team has 5 business days to review."),
|
||
.init(badge: "CALL", summary: "Budget discussion – CFO confirmed", when: "Mon",
|
||
detail: "Budget ceiling confirmed at AED 15M+. CFO expressed strong preference for a penthouse unit with private terrace."),
|
||
.init(badge: "VISIT", summary: "Site walkthrough – Penthouse Suite", when: "Last week",
|
||
detail: "First in-person visit. 45-minute walkthrough of Penthouse A & B. Visitor dwell time analysis showed 'excited' sentiment for 90% of the visit."),
|
||
]
|
||
|
||
struct RegionPin: Identifiable {
|
||
let id = UUID()
|
||
let label: String; let country: String; let count: Int; let temp: String; let topLead: String
|
||
}
|
||
private let mapPins: [RegionPin] = [
|
||
.init(label: "UAE", country: "🇦🇪", count: 8, temp: "hot", topLead: "Mohammed Al-Rashid"),
|
||
.init(label: "Saudi Arabia", country: "🇸🇦", count: 5, temp: "warm", topLead: "Al-Mansour Group"),
|
||
.init(label: "UK", country: "🇬🇧", count: 3, temp: "cold", topLead: "Rexford Capital"),
|
||
.init(label: "USA", country: "🇺🇸", count: 4, temp: "warm", topLead: "Apex Innovations"),
|
||
.init(label: "India", country: "🇮🇳", count: 6, temp: "hot", topLead: "Starlight Systems"),
|
||
.init(label: "Germany", country: "🇩🇪", count: 2, temp: "cold", topLead: "TechWave GmbH"),
|
||
]
|
||
|
||
struct CalTask: Identifiable {
|
||
let id = UUID()
|
||
let title: String; let subtitle: String; let due: String
|
||
}
|
||
private let calTasks: [CalTask] = [
|
||
.init(title: "Follow up with Mohammed", subtitle: "High-value penthouse lead – 2 unread messages", due: "Today 3 PM"),
|
||
.init(title: "Send contract to Fatima", subtitle: "3BR unit finalised – payment plan to confirm", due: "Tomorrow 10 AM"),
|
||
.init(title: "Schedule VR tour – James", subtitle: "Website lead, potential 1BR investor", due: "Thu 2 PM"),
|
||
]
|
||
|
||
// MARK: – OracleView (main)
|
||
struct OracleView: View {
|
||
@State private var selectedMode: OracleMode = .pipeline
|
||
@State private var prompt = "Show me a pipeline view by stage for Q4."
|
||
@State private var insightText = "Pipeline is healthy. Mohammed Al-Rashid is your highest-value close opportunity — follow up within 24 hours."
|
||
@State private var isSubmitting = false
|
||
|
||
// Sheet states
|
||
@State private var selectedLead: OracleLeadCard? = nil
|
||
@State private var selectedMember: TeamMemberData? = nil
|
||
@State private var selectedRegion: RegionPin? = nil
|
||
@State private var scheduledTask: CalTask? = nil
|
||
@State private var showScheduleConfirm = false
|
||
|
||
var body: some View {
|
||
ZStack(alignment: .bottom) {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
pageHeader
|
||
.padding(.horizontal, 24).padding(.top, 24).padding(.bottom, 16)
|
||
|
||
insightCard
|
||
.padding(.horizontal, 24).padding(.bottom, 14)
|
||
|
||
ScrollView {
|
||
canvasView
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, 120)
|
||
}
|
||
}
|
||
|
||
promptBar
|
||
.padding(.horizontal, 20)
|
||
.padding(.bottom, 12)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||
.background(VelocityTheme.background)
|
||
|
||
// Lead detail sheet
|
||
.sheet(item: $selectedLead) { card in
|
||
LeadDetailSheet(card: card)
|
||
}
|
||
// Team member sheet
|
||
.sheet(item: $selectedMember) { member in
|
||
MemberDetailSheet(member: member)
|
||
}
|
||
// Region callout sheet
|
||
.sheet(item: $selectedRegion) { pin in
|
||
RegionDetailSheet(pin: pin)
|
||
}
|
||
// Schedule confirmation alert
|
||
.alert("Confirm Schedule",
|
||
isPresented: $showScheduleConfirm,
|
||
presenting: scheduledTask) { task in
|
||
Button("Schedule") {
|
||
// In a real app this would create a calendar event
|
||
}
|
||
Button("Cancel", role: .cancel) {}
|
||
} message: { task in
|
||
Text("Add \"\(task.title)\" to your calendar for \(task.due)?")
|
||
}
|
||
}
|
||
|
||
// MARK: – Sub-views
|
||
private var pageHeader: some View {
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: 3) {
|
||
Text("Oracle").font(.system(size: 28, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
Text("AI intelligence pipeline").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
Spacer()
|
||
if isSubmitting {
|
||
ProgressView()
|
||
.progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent))
|
||
.scaleEffect(0.8)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var insightCard: some View {
|
||
HStack(alignment: .center, spacing: 0) {
|
||
RoundedRectangle(cornerRadius: 2)
|
||
.fill(LinearGradient(colors: [Color(red: 0.58, green: 0.77, blue: 0.99), VelocityTheme.accent],
|
||
startPoint: .top, endPoint: .bottom))
|
||
.frame(width: 3)
|
||
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: 3) {
|
||
Text("AI INSIGHT").font(.system(size: 9, weight: .semibold)).tracking(1.5)
|
||
.foregroundStyle(VelocityTheme.accent)
|
||
Text(insightText).font(.system(size: 13)).foregroundStyle(VelocityTheme.foreground).lineLimit(2)
|
||
}
|
||
Spacer()
|
||
HStack(spacing: 5) {
|
||
Image(systemName: selectedMode.icon).font(.system(size: 11)).foregroundStyle(VelocityTheme.accent)
|
||
Text(selectedMode.rawValue).font(.system(size: 11, weight: .medium))
|
||
.foregroundStyle(Color(red: 0.58, green: 0.77, blue: 0.99))
|
||
}
|
||
}
|
||
.padding(.horizontal, 14).padding(.vertical, 12)
|
||
}
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 12)
|
||
.fill(Color(red: 0.09, green: 0.15, blue: 0.33).opacity(0.55))
|
||
.overlay(RoundedRectangle(cornerRadius: 12).stroke(VelocityTheme.borderAccent, lineWidth: 1))
|
||
)
|
||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var canvasView: some View {
|
||
switch selectedMode {
|
||
case .pipeline:
|
||
PipelineCanvas(onSelectLead: { selectedLead = $0 })
|
||
case .teamPerformance:
|
||
TeamPerformanceCanvas(onSelectMember: { selectedMember = $0 })
|
||
case .accountTimeline:
|
||
AccountTimelineCanvas()
|
||
case .leadMap:
|
||
LeadMapCanvas(onSelectRegion: { selectedRegion = $0 })
|
||
case .calendarTasks:
|
||
CalendarCanvas(onSchedule: { task in
|
||
scheduledTask = task
|
||
showScheduleConfirm = true
|
||
})
|
||
}
|
||
}
|
||
|
||
// MARK: – Prompt Bar
|
||
private var promptBar: some View {
|
||
VStack(spacing: 0) {
|
||
TextField("Ask Oracle anything…", text: $prompt)
|
||
.font(.system(size: 14))
|
||
.foregroundStyle(VelocityTheme.foreground)
|
||
.tint(VelocityTheme.accent)
|
||
.onSubmit { submitPrompt() }
|
||
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
|
||
|
||
HStack {
|
||
Menu {
|
||
ForEach(OracleMode.allCases, id: \.self) { mode in
|
||
Button {
|
||
selectedMode = mode
|
||
prompt = modeSamplePrompt(mode)
|
||
insightText = oracleInsight(for: mode)
|
||
} label: {
|
||
Label(mode.rawValue, systemImage: mode.icon)
|
||
}
|
||
}
|
||
} label: {
|
||
HStack(spacing: 5) {
|
||
Image(systemName: selectedMode.icon).font(.system(size: 10))
|
||
Text(selectedMode.rawValue).font(.system(size: 11, weight: .medium))
|
||
Image(systemName: "chevron.down").font(.system(size: 8))
|
||
}
|
||
.foregroundStyle(Color(red: 0.58, green: 0.77, blue: 0.99))
|
||
.padding(.horizontal, 10).padding(.vertical, 6)
|
||
.background(Capsule().fill(VelocityTheme.accent.opacity(0.14))
|
||
.overlay(Capsule().stroke(VelocityTheme.accent.opacity(0.3), lineWidth: 1)))
|
||
}
|
||
Spacer()
|
||
Button(action: submitPrompt) {
|
||
ZStack {
|
||
Circle()
|
||
.fill(isSubmitting ? VelocityTheme.mutedFg : VelocityTheme.accent)
|
||
.shadow(color: VelocityTheme.accent.opacity(0.5), radius: 8)
|
||
if isSubmitting {
|
||
ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .white)).scaleEffect(0.6)
|
||
} else {
|
||
Image(systemName: "paperplane.fill").font(.system(size: 12)).foregroundStyle(.white)
|
||
}
|
||
}
|
||
.frame(width: 34, height: 34)
|
||
}
|
||
.disabled(isSubmitting || prompt.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
}
|
||
.padding(.horizontal, 12).padding(.bottom, 12)
|
||
}
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 18)
|
||
.fill(Color(red: 0.039, green: 0.043, blue: 0.063).opacity(0.95))
|
||
.overlay(RoundedRectangle(cornerRadius: 18).stroke(Color.white.opacity(0.11), lineWidth: 1))
|
||
.shadow(color: .black.opacity(0.6), radius: 20, y: -4)
|
||
)
|
||
}
|
||
|
||
// MARK: – Prompt logic
|
||
private func submitPrompt() {
|
||
let clean = prompt.trimmingCharacters(in: .whitespaces)
|
||
guard !clean.isEmpty && !isSubmitting else { return }
|
||
isSubmitting = true
|
||
let lower = clean.lowercased()
|
||
if lower.contains("team") || lower.contains("performance") || lower.contains("sales") {
|
||
selectedMode = .teamPerformance
|
||
} else if lower.contains("account") || lower.contains("apex") || lower.contains("timeline") {
|
||
selectedMode = .accountTimeline
|
||
} else if lower.contains("map") || lower.contains("geographic") || lower.contains("location") {
|
||
selectedMode = .leadMap
|
||
} else if lower.contains("calendar") || lower.contains("schedule") || lower.contains("task") {
|
||
selectedMode = .calendarTasks
|
||
} else {
|
||
selectedMode = .pipeline
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
|
||
withAnimation(.easeInOut(duration: 0.3)) {
|
||
insightText = oracleInsight(for: selectedMode)
|
||
isSubmitting = false
|
||
}
|
||
}
|
||
}
|
||
|
||
private func modeSamplePrompt(_ mode: OracleMode) -> String {
|
||
switch mode {
|
||
case .pipeline: return "Show me a pipeline view by stage for Q4."
|
||
case .teamPerformance: return "What's the performance of the sales team this month?"
|
||
case .accountTimeline: return "Find all contacts at Apex Innovations and their recent activity."
|
||
case .leadMap: return "Give me a geographic map of all leads."
|
||
case .calendarTasks: return "Schedule follow-ups with the top 3 high-value leads."
|
||
}
|
||
}
|
||
|
||
private func oracleInsight(for mode: OracleMode) -> String {
|
||
switch mode {
|
||
case .pipeline: return "Pipeline is healthy. Mohammed Al-Rashid is your highest-value close opportunity — follow up within 24 hours."
|
||
case .teamPerformance: return "Rania Al-Farsi leads the team at $2.1M closed. Overall quota attainment is at 87% — ahead of last month."
|
||
case .accountTimeline: return "Apex Innovations has 4 active stakeholders. Confusion detected in legal stage — recommend expediting contract review."
|
||
case .leadMap: return "UAE and India show the hottest lead concentration. 8 high-value prospects in UAE require immediate outreach."
|
||
case .calendarTasks: return "3 high-priority follow-ups scheduled. Mohammed Al-Rashid has 2 unread messages — respond within 24h."
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Pipeline Canvas
|
||
private struct PipelineCanvas: View {
|
||
let onSelectLead: (OracleLeadCard) -> Void
|
||
private let cols = [GridItem(.adaptive(minimum: 200), spacing: 12)]
|
||
|
||
var body: some View {
|
||
LazyVGrid(columns: cols, alignment: .leading, spacing: 12) {
|
||
ForEach(pipelineData, id: \.stage) { col in
|
||
VStack(alignment: .leading, spacing: 10) {
|
||
HStack {
|
||
Text(col.stage.uppercased())
|
||
.font(.system(size: 10, weight: .semibold)).tracking(1)
|
||
.foregroundStyle(VelocityTheme.mutedFg)
|
||
Spacer()
|
||
Text("\(col.cards.count)")
|
||
.font(.system(size: 10, weight: .semibold))
|
||
.foregroundStyle(VelocityTheme.accent)
|
||
.padding(.horizontal, 7).padding(.vertical, 3)
|
||
.background(Capsule().fill(VelocityTheme.accent.opacity(0.12))
|
||
.overlay(Capsule().stroke(VelocityTheme.accent.opacity(0.2), lineWidth: 1)))
|
||
}
|
||
ForEach(col.cards) { card in
|
||
TappableLeadCard(card: card, onTap: { onSelectLead(card) })
|
||
}
|
||
}
|
||
.padding(16)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 14)
|
||
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
|
||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderSubtle, lineWidth: 1))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct TappableLeadCard: View {
|
||
let card: OracleLeadCard
|
||
let onTap: () -> Void
|
||
@State private var pressed = false
|
||
|
||
var body: some View {
|
||
HStack(spacing: 10) {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 8).fill(card.status.color.opacity(0.18)).frame(width: 36, height: 36)
|
||
Text(card.initials).font(.system(size: 12, weight: .bold)).foregroundStyle(card.status.color)
|
||
}
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(card.name).font(.system(size: 12, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
|
||
Text(card.company).font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
Spacer()
|
||
VStack(alignment: .trailing, spacing: 2) {
|
||
Text(card.value).font(.system(size: 11, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
|
||
Image(systemName: "chevron.right").font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
}
|
||
.padding(10)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 10)
|
||
.fill(pressed ? VelocityTheme.accent.opacity(0.10) : Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||
.stroke(pressed ? VelocityTheme.accent.opacity(0.35) : Color.white.opacity(0.06), lineWidth: 1))
|
||
)
|
||
.scaleEffect(pressed ? 0.97 : 1.0)
|
||
.animation(.easeInOut(duration: 0.12), value: pressed)
|
||
.onTapGesture {
|
||
pressed = true
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
|
||
onTap()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Lead Detail Sheet
|
||
private struct LeadDetailSheet: View {
|
||
let card: OracleLeadCard
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
VStack(alignment: .leading, spacing: 20) {
|
||
// Avatar + name
|
||
HStack(spacing: 16) {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 14).fill(card.status.color.opacity(0.20)).frame(width: 60, height: 60)
|
||
Text(card.initials).font(.system(size: 22, weight: .bold)).foregroundStyle(card.status.color)
|
||
}
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(card.name).font(.system(size: 20, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
HStack(spacing: 6) {
|
||
Text(card.status.rawValue)
|
||
.font(.system(size: 11, weight: .semibold))
|
||
.foregroundStyle(card.status.color)
|
||
.padding(.horizontal, 8).padding(.vertical, 3)
|
||
.background(Capsule().fill(card.status.color.opacity(0.14)))
|
||
Text(card.qualification).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
}
|
||
}
|
||
.padding(.top, 8)
|
||
|
||
Divider().background(VelocityTheme.borderSubtle)
|
||
|
||
// Details grid
|
||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
|
||
DetailField(label: "Deal Value", value: card.value)
|
||
DetailField(label: "Source", value: card.company)
|
||
DetailField(label: "Interest", value: card.interest)
|
||
DetailField(label: "Phone", value: card.phone)
|
||
}
|
||
|
||
Divider().background(VelocityTheme.borderSubtle)
|
||
|
||
// Action buttons
|
||
HStack(spacing: 12) {
|
||
ActionChip(icon: "phone.fill", label: "Call", color: VelocityTheme.success)
|
||
ActionChip(icon: "message.fill", label: "Message", color: VelocityTheme.accent)
|
||
ActionChip(icon: "calendar.badge.plus", label: "Schedule", color: Color(red: 0.60, green: 0.57, blue: 0.99))
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
.padding(24)
|
||
.background(VelocityTheme.background.ignoresSafeArea())
|
||
.navigationTitle("Lead Details")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .navigationBarTrailing) {
|
||
Button("Done") { dismiss() }
|
||
.foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct DetailField: View {
|
||
let label: String; let value: String
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(label.uppercased()).font(.system(size: 9, weight: .semibold)).tracking(1)
|
||
.foregroundStyle(VelocityTheme.mutedFg)
|
||
Text(value).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
|
||
}
|
||
.padding(12)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.07), lineWidth: 1)))
|
||
}
|
||
}
|
||
|
||
private struct ActionChip: View {
|
||
let icon: String; let label: String; let color: Color
|
||
@State private var pressed = false
|
||
var body: some View {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: icon).font(.system(size: 12))
|
||
Text(label).font(.system(size: 12, weight: .semibold))
|
||
}
|
||
.foregroundStyle(color)
|
||
.padding(.horizontal, 16).padding(.vertical, 10)
|
||
.frame(maxWidth: .infinity)
|
||
.background(RoundedRectangle(cornerRadius: 10).fill(color.opacity(pressed ? 0.22 : 0.13))
|
||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(color.opacity(0.30), lineWidth: 1)))
|
||
.scaleEffect(pressed ? 0.96 : 1.0)
|
||
.animation(.easeInOut(duration: 0.12), value: pressed)
|
||
.onTapGesture { pressed = true; DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { pressed = false } }
|
||
}
|
||
}
|
||
|
||
// MARK: – Team Performance Canvas
|
||
private struct TeamPerformanceCanvas: View {
|
||
let onSelectMember: (TeamMemberData) -> Void
|
||
|
||
var body: some View {
|
||
VStack(spacing: 14) {
|
||
quotaPanel
|
||
teamListPanel
|
||
}
|
||
}
|
||
|
||
private var quotaPanel: some View {
|
||
HStack(spacing: 14) {
|
||
ZStack {
|
||
Circle().stroke(Color.white.opacity(0.06), lineWidth: 10).frame(width: 110, height: 110)
|
||
Circle()
|
||
.trim(from: 0, to: 0.87)
|
||
.stroke(AngularGradient(colors: [VelocityTheme.accent, Color(red: 0.13, green: 0.83, blue: 0.93)],
|
||
center: .center),
|
||
style: StrokeStyle(lineWidth: 10, lineCap: .round))
|
||
.rotationEffect(.degrees(-90))
|
||
.frame(width: 110, height: 110)
|
||
VStack(spacing: 2) {
|
||
Text("87%").font(.system(size: 26, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
Text("QUOTA").font(.system(size: 8, weight: .semibold)).tracking(1.2).foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
}
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Quota Attainment").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
|
||
Text("Monthly target exceeded").font(.system(size: 11)).foregroundStyle(VelocityTheme.success)
|
||
Text("Q4 FY2025–26").font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(20)
|
||
.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 teamListPanel: some View {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text("TEAM PERFORMANCE").font(.system(size: 10, weight: .semibold)).tracking(1.2)
|
||
.foregroundStyle(VelocityTheme.mutedFg).padding(.bottom, 8)
|
||
ForEach(teamData) { member in
|
||
TappableTeamRow(member: member, onTap: { onSelectMember(member) })
|
||
}
|
||
}
|
||
.padding(20)
|
||
.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 struct TappableTeamRow: View {
|
||
let member: TeamMemberData
|
||
let onTap: () -> Void
|
||
@State private var pressed = false
|
||
|
||
var body: some View {
|
||
HStack(spacing: 12) {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 8).fill(VelocityTheme.accent.opacity(0.18)).frame(width: 36, height: 36)
|
||
Text(member.initials).font(.system(size: 12, weight: .bold)).foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(member.name).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
|
||
Text("\(member.deals) deals closed").font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
Spacer()
|
||
VStack(alignment: .trailing, spacing: 2) {
|
||
Text(member.revenue).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
|
||
Text(member.trend)
|
||
.font(.system(size: 10, weight: .medium))
|
||
.foregroundStyle(member.trend.hasPrefix("↑") ? VelocityTheme.success :
|
||
member.trend.hasPrefix("↓") ? VelocityTheme.danger : VelocityTheme.mutedFg)
|
||
}
|
||
Image(systemName: "chevron.right").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
.padding(12)
|
||
.background(RoundedRectangle(cornerRadius: 10)
|
||
.fill(pressed ? VelocityTheme.accent.opacity(0.08) : Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||
.stroke(pressed ? VelocityTheme.accent.opacity(0.25) : Color.white.opacity(0.06), lineWidth: 1)))
|
||
.scaleEffect(pressed ? 0.98 : 1.0)
|
||
.animation(.easeInOut(duration: 0.12), value: pressed)
|
||
.onTapGesture {
|
||
pressed = true
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
|
||
onTap()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Team Member Detail Sheet
|
||
private struct MemberDetailSheet: View {
|
||
let member: TeamMemberData
|
||
@Environment(\.dismiss) private var dismiss
|
||
var body: some View {
|
||
NavigationStack {
|
||
VStack(alignment: .leading, spacing: 20) {
|
||
HStack(spacing: 16) {
|
||
ZStack {
|
||
RoundedRectangle(cornerRadius: 14).fill(VelocityTheme.accent.opacity(0.18)).frame(width: 60, height: 60)
|
||
Text(member.initials).font(.system(size: 22, weight: .bold)).foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(member.name).font(.system(size: 20, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
Text("Sales Executive").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
}
|
||
.padding(.top, 8)
|
||
Divider().background(VelocityTheme.borderSubtle)
|
||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
|
||
DetailField(label: "Revenue Closed", value: member.revenue)
|
||
DetailField(label: "Deals Closed", value: "\(member.deals)")
|
||
DetailField(label: "Trend", value: member.trend)
|
||
DetailField(label: "Period", value: "Q4 FY2025–26")
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(24)
|
||
.background(VelocityTheme.background.ignoresSafeArea())
|
||
.navigationTitle("Team Member")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .navigationBarTrailing) {
|
||
Button("Done") { dismiss() }.foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Account Timeline Canvas
|
||
private struct AccountTimelineCanvas: View {
|
||
@State private var expandedId: UUID? = nil
|
||
|
||
var body: some View {
|
||
VStack(spacing: 14) {
|
||
// Account overview
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
Text("ACCOUNT OVERVIEW").font(.system(size: 9, weight: .semibold)).tracking(1.5).foregroundStyle(VelocityTheme.accent)
|
||
Text("Apex Innovations").font(.system(size: 24, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
HStack(spacing: 14) {
|
||
InfoMini(label: "Deal Value", value: "AED 15M+")
|
||
InfoMini(label: "Primary Contact", value: "CEO – James T.")
|
||
InfoMini(label: "Industry", value: "Technology")
|
||
}
|
||
}
|
||
.padding(20)
|
||
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
|
||
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
|
||
|
||
// Expandable timeline
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
Text("Activity Timeline").font(.system(size: 13, weight: .semibold))
|
||
.foregroundStyle(VelocityTheme.foreground).padding(.bottom, 16)
|
||
ForEach(Array(timelineEvents.enumerated()), id: \.offset) { i, event in
|
||
TimelineEventRow(event: event, isLast: i == timelineEvents.count - 1,
|
||
isExpanded: expandedId == event.id) {
|
||
withAnimation(.easeInOut(duration: 0.25)) {
|
||
expandedId = expandedId == event.id ? nil : event.id
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(20)
|
||
.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 struct TimelineEventRow: View {
|
||
let event: OracleTimelineEvent
|
||
let isLast: Bool
|
||
let isExpanded: Bool
|
||
let onTap: () -> Void
|
||
|
||
var body: some View {
|
||
HStack(alignment: .top, spacing: 14) {
|
||
VStack(spacing: 0) {
|
||
Circle().fill(VelocityTheme.accent).frame(width: 10, height: 10)
|
||
.overlay(Circle().stroke(VelocityTheme.background, lineWidth: 2))
|
||
if !isLast {
|
||
Rectangle()
|
||
.fill(LinearGradient(colors: [VelocityTheme.accent.opacity(0.5), .clear],
|
||
startPoint: .top, endPoint: .bottom))
|
||
.frame(width: 2)
|
||
.frame(height: isExpanded ? 100 : 50)
|
||
.animation(.easeInOut(duration: 0.25), value: isExpanded)
|
||
}
|
||
}
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
HStack {
|
||
Text(event.badge).font(.system(size: 9, weight: .bold)).tracking(1.2)
|
||
.foregroundStyle(VelocityTheme.accent)
|
||
.padding(.horizontal, 7).padding(.vertical, 3)
|
||
.background(RoundedRectangle(cornerRadius: 4).fill(VelocityTheme.accent.opacity(0.15))
|
||
.overlay(RoundedRectangle(cornerRadius: 4).stroke(VelocityTheme.accent.opacity(0.25), lineWidth: 1)))
|
||
Spacer()
|
||
Text(event.when).font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
|
||
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
||
.font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
Text(event.summary).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
|
||
if isExpanded {
|
||
Text(event.detail).font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
|
||
.padding(.top, 4)
|
||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||
}
|
||
}
|
||
.padding(12)
|
||
.background(RoundedRectangle(cornerRadius: 10)
|
||
.fill(isExpanded ? VelocityTheme.accent.opacity(0.06) : Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||
.stroke(isExpanded ? VelocityTheme.accent.opacity(0.2) : Color.white.opacity(0.06), lineWidth: 1)))
|
||
.onTapGesture { onTap() }
|
||
.padding(.bottom, 8)
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct InfoMini: View {
|
||
let label: String; let value: String
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(label).font(.system(size: 9)).tracking(0.8).foregroundStyle(VelocityTheme.mutedFg)
|
||
Text(value).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
.padding(10)
|
||
.background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.white.opacity(0.06), lineWidth: 1)))
|
||
}
|
||
}
|
||
|
||
// MARK: – Lead Map Canvas
|
||
private struct LeadMapCanvas: View {
|
||
let onSelectRegion: (RegionPin) -> Void
|
||
private let cols = [GridItem(.adaptive(minimum: 140), spacing: 10)]
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 14) {
|
||
HStack(spacing: 16) {
|
||
LegendDot(color: VelocityTheme.danger, label: "Hot Lead")
|
||
LegendDot(color: Color(red: 0.13, green: 0.83, blue: 0.93), label: "Warm Lead")
|
||
LegendDot(color: VelocityTheme.mutedFg, label: "Cold Lead")
|
||
Spacer()
|
||
}
|
||
LazyVGrid(columns: cols, spacing: 10) {
|
||
ForEach(mapPins) { pin in
|
||
TappableRegionPin(pin: pin, onTap: { onSelectRegion(pin) })
|
||
}
|
||
}
|
||
}
|
||
.padding(20)
|
||
.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 struct TappableRegionPin: View {
|
||
let pin: RegionPin
|
||
let onTap: () -> Void
|
||
@State private var pressed = false
|
||
|
||
private var pinColor: Color {
|
||
pin.temp == "hot" ? VelocityTheme.danger :
|
||
pin.temp == "warm" ? Color(red: 0.13, green: 0.83, blue: 0.93) : VelocityTheme.mutedFg
|
||
}
|
||
|
||
var body: some View {
|
||
HStack(spacing: 10) {
|
||
Text(pin.country).font(.system(size: 24))
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(pin.label).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
|
||
HStack(spacing: 4) {
|
||
Circle().fill(pinColor).frame(width: 6, height: 6).shadow(color: pinColor.opacity(0.8), radius: 3)
|
||
Text("\(pin.count) leads").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
}
|
||
Spacer()
|
||
Image(systemName: "arrow.up.right.circle")
|
||
.font(.system(size: 13)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
.padding(12)
|
||
.background(RoundedRectangle(cornerRadius: 10)
|
||
.fill(pressed ? pinColor.opacity(0.12) : Color(red: 0.031, green: 0.039, blue: 0.071))
|
||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(pinColor.opacity(pressed ? 0.5 : 0.25), lineWidth: 1)))
|
||
.scaleEffect(pressed ? 0.97 : 1.0)
|
||
.animation(.easeInOut(duration: 0.12), value: pressed)
|
||
.onTapGesture {
|
||
pressed = true
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
|
||
onTap()
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct LegendDot: View {
|
||
let color: Color; let label: String
|
||
var body: some View {
|
||
HStack(spacing: 6) {
|
||
Circle().fill(color).frame(width: 8, height: 8).shadow(color: color.opacity(0.8), radius: 3)
|
||
Text(label).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Region Detail Sheet
|
||
private struct RegionDetailSheet: View {
|
||
let pin: RegionPin
|
||
@Environment(\.dismiss) private var dismiss
|
||
private var pinColor: Color {
|
||
pin.temp == "hot" ? VelocityTheme.danger :
|
||
pin.temp == "warm" ? Color(red: 0.13, green: 0.83, blue: 0.93) : VelocityTheme.mutedFg
|
||
}
|
||
var body: some View {
|
||
NavigationStack {
|
||
VStack(alignment: .leading, spacing: 20) {
|
||
HStack(spacing: 16) {
|
||
Text(pin.country).font(.system(size: 52))
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text(pin.label).font(.system(size: 22, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
|
||
HStack(spacing: 6) {
|
||
Circle().fill(pinColor).frame(width: 7, height: 7)
|
||
Text(pin.temp.capitalized + " Market")
|
||
.font(.system(size: 12, weight: .medium)).foregroundStyle(pinColor)
|
||
}
|
||
}
|
||
}
|
||
.padding(.top, 8)
|
||
Divider().background(VelocityTheme.borderSubtle)
|
||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
|
||
DetailField(label: "Active Leads", value: "\(pin.count)")
|
||
DetailField(label: "Top Lead", value: pin.topLead)
|
||
DetailField(label: "Temperature", value: pin.temp.capitalized)
|
||
DetailField(label: "Priority", value: pin.temp == "hot" ? "High 🔴" : pin.temp == "warm" ? "Medium 🟡" : "Low ⚪")
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(24)
|
||
.background(VelocityTheme.background.ignoresSafeArea())
|
||
.navigationTitle("Region Details")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .navigationBarTrailing) {
|
||
Button("Done") { dismiss() }.foregroundStyle(VelocityTheme.accent)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: – Calendar Canvas
|
||
private struct CalendarCanvas: View {
|
||
let onSchedule: (CalTask) -> Void
|
||
let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||
|
||
var body: some View {
|
||
VStack(spacing: 14) {
|
||
weekPanel
|
||
tasksPanel
|
||
}
|
||
}
|
||
|
||
private var weekPanel: some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
Text("Weekly Schedule").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
|
||
HStack(spacing: 6) {
|
||
ForEach(days, id: \.self) { day in
|
||
Text(day).font(.system(size: 10, weight: .medium)).tracking(0.5)
|
||
.foregroundStyle(VelocityTheme.mutedFg).frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
HStack(spacing: 6) {
|
||
ForEach(Array(days.enumerated()), id: \.offset) { i, _ in
|
||
RoundedRectangle(cornerRadius: 8)
|
||
.fill(i == 2 ? VelocityTheme.accent.opacity(0.15) : Color.white.opacity(0.03))
|
||
.overlay(RoundedRectangle(cornerRadius: 8)
|
||
.stroke(i == 2 ? VelocityTheme.accent.opacity(0.3) : Color.white.opacity(0.05), lineWidth: 1))
|
||
.frame(height: 60)
|
||
}
|
||
}
|
||
}
|
||
.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 tasksPanel: some View {
|
||
VStack(alignment: .leading, spacing: 10) {
|
||
HStack(spacing: 5) {
|
||
Circle().fill(Color(red: 0.13, green: 0.83, blue: 0.93)).frame(width: 6, height: 6)
|
||
Text("Tasks & Actions").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
|
||
}
|
||
.padding(.bottom, 4)
|
||
ForEach(calTasks) { task in
|
||
CalTaskRow(task: task, onSchedule: { onSchedule(task) })
|
||
}
|
||
}
|
||
.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 struct CalTaskRow: View {
|
||
let task: CalTask
|
||
let onSchedule: () -> Void
|
||
@State private var scheduled = false
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
HStack {
|
||
Text(task.title).font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
|
||
Spacer()
|
||
Text(scheduled ? "Scheduled ✓" : "Action")
|
||
.font(.system(size: 9, weight: .semibold))
|
||
.foregroundStyle(scheduled ? VelocityTheme.success : VelocityTheme.mutedFg)
|
||
.padding(.horizontal, 6).padding(.vertical, 3)
|
||
.background(RoundedRectangle(cornerRadius: 4)
|
||
.fill(scheduled ? VelocityTheme.success.opacity(0.12) : Color.white.opacity(0.06)))
|
||
}
|
||
Text(task.subtitle).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg).lineLimit(2)
|
||
HStack {
|
||
Image(systemName: "clock").font(.system(size: 10)).foregroundStyle(VelocityTheme.accent)
|
||
Text(task.due).font(.system(size: 11)).foregroundStyle(VelocityTheme.accent)
|
||
Spacer()
|
||
Button {
|
||
onSchedule()
|
||
withAnimation(.easeInOut(duration: 0.3)) { scheduled = true }
|
||
} label: {
|
||
HStack(spacing: 5) {
|
||
Image(systemName: scheduled ? "checkmark" : "calendar.badge.plus")
|
||
.font(.system(size: 10, weight: .semibold))
|
||
Text(scheduled ? "Scheduled" : "Schedule")
|
||
.font(.system(size: 11, weight: .semibold))
|
||
}
|
||
.foregroundStyle(.white)
|
||
.padding(.horizontal, 12).padding(.vertical, 5)
|
||
.background(RoundedRectangle(cornerRadius: 7)
|
||
.fill(scheduled ? VelocityTheme.success : VelocityTheme.accent)
|
||
.shadow(color: (scheduled ? VelocityTheme.success : VelocityTheme.accent).opacity(0.4), radius: 6))
|
||
}
|
||
}
|
||
}
|
||
.padding(12)
|
||
.background(RoundedRectangle(cornerRadius: 10)
|
||
.fill(scheduled ? VelocityTheme.success.opacity(0.05) : Color.white.opacity(0.04))
|
||
.overlay(RoundedRectangle(cornerRadius: 10)
|
||
.stroke(scheduled ? VelocityTheme.success.opacity(0.2) : Color.white.opacity(0.06), lineWidth: 1)))
|
||
}
|
||
}
|