1→import SwiftUI 2→import Charts 3→ 4→struct PIXStatsView: View { 5→ @EnvironmentObject var viewModel: DashboardViewModel 6→ 7→ var body: some View { 8→ ScrollView { 9→ VStack(alignment: .leading, spacing: 24) { 10→ // Header 11→ PIXHeader(isLoading: viewModel.pixLoading) 12→ 13→ // Stats Cards 14→ PIXStatsCards(stats: viewModel.pixStats) 15→ 16→ // Charts Row 17→ HStack(spacing: 16) { 18→ // Daily Volume Chart 19→ PIXDailyChart(data: viewModel.pixStats.last30DaysData) 20→ .frame(minHeight: 300) 21→ 22→ // Intraday Chart 23→ PIXIntradayChart(data: viewModel.pixStats.intradayData) 24→ .frame(minHeight: 300) 25→ } 26→ .padding(.horizontal) 27→ 28→ // Bottom Row 29→ HStack(spacing: 16) { 30→ // Availability Chart 31→ PIXAvailabilityCard(data: viewModel.pixStats.availabilityData) 32→ 33→ // Remuneration Info 34→ PIXRemunerationCard(data: viewModel.pixStats.remunerationData) 35→ } 36→ .padding(.horizontal) 37→ 38→ // Recent Transactions Table 39→ PIXRecentTable(data: viewModel.pixStats.dailyData) 40→ .padding(.horizontal) 41→ 42→ Spacer(minLength: 20) 43→ } 44→ } 45→ .background(Color(NSColor.windowBackgroundColor)) 46→ .task { 47→ if viewModel.pixStats.dailyData.isEmpty { 48→ await viewModel.loadPIXStats() 49→ } 50→ } 51→ } 52→} 53→ 54→// MARK: - Header 55→ 56→struct PIXHeader: View { 57→ let isLoading: Bool 58→ 59→ var body: some View { 60→ HStack { 61→ Image(systemName: "qrcode") 62→ .font(.system(size: 32)) 63→ .foregroundStyle(Color.bcbGreen) 64→ 65→ VStack(alignment: .leading, spacing: 4) { 66→ Text("PIX - Sistema de Pagamentos Instantâneos") 67→ .font(.title2) 68→ .fontWeight(.bold) 69→ 70→ Text("Dados em tempo real do SPI - Banco Central") 71→ .font(.subheadline) 72→ .foregroundStyle(.secondary) 73→ } 74→ 75→ Spacer() 76→ 77→ if isLoading { 78→ ProgressView() 79→ .controlSize(.small) 80→ } 81→ 82→ Image(systemName: "checkmark.seal.fill") 83→ .font(.title2) 84→ .foregroundStyle(.green) 85→ .help("Dados oficiais do BCB") 86→ } 87→ .padding() 88→ } 89→} 90→ 91→// MARK: - Stats Cards 92→ 93→struct PIXStatsCards: View { 94→ let stats: PIXStats 95→ 96→ var body: some View { 97→ LazyVGrid(columns: [ 98→ GridItem(.flexible()), 99→ GridItem(.flexible()), 100→ GridItem(.flexible()), 101→ GridItem(.flexible()) 102→ ], spacing: 16) { 103→ PIXStatCard( 104→ title: "Transações (último dia)", 105→ value: stats.latestDaily?.formattedQuantity ?? "--", 106→ subtitle: stats.latestDaily?.formattedDate ?? "", 107→ icon: "arrow.left.arrow.right", 108→ color: .bcbGreen 109→ ) 110→ 111→ PIXStatCard( 112→ title: "Volume (último dia)", 113→ value: stats.latestDaily?.formattedTotal ?? "--", 114→ subtitle: "em milhares de R$", 115→ icon: "brazilianrealsign.circle.fill", 116→ color: .bcbBlue 117→ ) 118→ 119→ PIXStatCard( 120→ title: "Ticket Médio", 121→ value: stats.latestDaily?.formattedAverage ?? "--", 122→ subtitle: "por transação", 123→ icon: "chart.bar.fill", 124→ color: .bcbPurple 125→ ) 126→ 127→ PIXStatCard( 128→ title: "Disponibilidade SPI", 129→ value: stats.availabilityData.isEmpty ? "--" : String(format: "%.2f%%", stats.averageAvailability), 130→ subtitle: stats.availabilityData.isEmpty ? "indisponível" : "média últimos 12 meses", 131→ icon: "checkmark.circle.fill", 132→ color: stats.availabilityData.isEmpty ? .gray : (stats.averageAvailability >= 99.9 ? .green : .orange) 133→ ) 134→ } 135→ .padding(.horizontal) 136→ } 137→} 138→ 139→struct PIXStatCard: View { 140→ let title: String 141→ let value: String 142→ let subtitle: String 143→ let icon: String 144→ let color: Color 145→ 146→ var body: some View { 147→ VStack(alignment: .leading, spacing: 12) { 148→ HStack { 149→ Image(systemName: icon) 150→ .font(.title2) 151→ .foregroundStyle(color) 152→ 153→ Spacer() 154→ } 155→ 156→ Text(value) 157→ .font(.system(size: 24, weight: .bold, design: .rounded)) 158→ .lineLimit(1) 159→ .minimumScaleFactor(0.7) 160→ 161→ VStack(alignment: .leading, spacing: 2) { 162→ Text(title) 163→ .font(.caption) 164→ .foregroundStyle(.secondary) 165→ .lineLimit(1) 166→ 167→ Text(subtitle) 168→ .font(.caption2) 169→ .foregroundStyle(.tertiary) 170→ .lineLimit(1) 171→ } 172→ } 173→ .padding() 174→ .background(color.opacity(0.1)) 175→ .clipShape(RoundedRectangle(cornerRadius: 12)) 176→ } 177→} 178→ 179→// MARK: - Daily Volume Chart 180→ 181→struct PIXDailyChart: View { 182→ let data: [PIXDaily] 183→ 184→ var body: some View { 185→ VStack(alignment: .leading, spacing: 12) { 186→ HStack { 187→ Text("Volume Diário de Transações") 188→ .font(.headline) 189→ 190→ Spacer() 191→ 192→ Text("Últimos 30 dias") 193→ .font(.caption) 194→ .foregroundStyle(.secondary) 195→ } 196→ 197→ if data.isEmpty { 198→ ContentUnavailableView("Carregando...", systemImage: "chart.line.uptrend.xyaxis") 199→ } else { 200→ Chart(data) { item in 201→ BarMark( 202→ x: .value("Data", item.parsedDate ?? Date()), 203→ y: .value("Quantidade", Double(item.quantity) / 1_000_000) 204→ ) 205→ .foregroundStyle(Color.bcbGreen.gradient) 206→ } 207→ .chartXAxis { 208→ AxisMarks(values: .stride(by: .day, count: 7)) { _ in 209→ AxisGridLine() 210→ AxisValueLabel(format: .dateTime.day().month(.abbreviated)) 211→ } 212→ } 213→ .chartYAxis { 214→ AxisMarks { value in 215→ AxisGridLine() 216→ AxisValueLabel { 217→ if let v = value.as(Double.self) { 218→ Text("\(Int(v))M") 219→ } 220→ } 221→ } 222→ } 223→ } 224→ } 225→ .padding() 226→ .background(Color(NSColor.controlBackgroundColor)) 227→ .clipShape(RoundedRectangle(cornerRadius: 12)) 228→ } 229→} 230→ 231→// MARK: - Intraday Chart 232→ 233→struct PIXIntradayChart: View { 234→ let data: [PIXIntraday] 235→ 236→ var sortedData: [PIXIntraday] { 237→ data.sorted { $0.time < $1.time } 238→ } 239→ 240→ var body: some View { 241→ VStack(alignment: .leading, spacing: 12) { 242→ HStack { 243→ Text("Distribuição por Horário") 244→ .font(.headline) 245→ 246→ Spacer() 247→ 248→ Text("Média de transações") 249→ .font(.caption) 250→ .foregroundStyle(.secondary) 251→ } 252→ 253→ if data.isEmpty { 254→ ContentUnavailableView("Carregando...", systemImage: "clock") 255→ } else { 256→ Chart(sortedData) { item in 257→ LineMark( 258→ x: .value("Horário", item.time), 259→ y: .value("Quantidade", Double(item.averageQuantity) / 1_000_000) 260→ ) 261→ .foregroundStyle(Color.bcbBlue) 262→ .interpolationMethod(.catmullRom) 263→ 264→ AreaMark( 265→ x: .value("Horário", item.time), 266→ y: .value("Quantidade", Double(item.averageQuantity) / 1_000_000) 267→ ) 268→ .foregroundStyle(Color.bcbBlue.opacity(0.1)) 269→ .interpolationMethod(.catmullRom) 270→ } 271→ .chartXAxis { 272→ AxisMarks(values: .stride(by: 6)) { value in 273→ AxisGridLine() 274→ AxisValueLabel() 275→ } 276→ } 277→ .chartYAxis { 278→ AxisMarks { value in 279→ AxisGridLine() 280→ AxisValueLabel { 281→ if let v = value.as(Double.self) { 282→ Text(String(format: "%.1fM", v)) 283→ } 284→ } 285→ } 286→ } 287→ } 288→ } 289→ .padding() 290→ .background(Color(NSColor.controlBackgroundColor)) 291→ .clipShape(RoundedRectangle(cornerRadius: 12)) 292→ } 293→} 294→ 295→// MARK: - Availability Card 296→ 297→struct PIXAvailabilityCard: View { 298→ let data: [PIXAvailability] 299→ 300→ var sortedData: [PIXAvailability] { 301→ data.sorted { ($0.parsedDate ?? Date.distantPast) < ($1.parsedDate ?? Date.distantPast) } 302→ } 303→ 304→ var body: some View { 305→ VStack(alignment: .leading, spacing: 12) { 306→ HStack { 307→ Text("Disponibilidade do SPI") 308→ .font(.headline) 309→ 310→ Spacer() 311→ 312→ if let latest = data.first { 313→ HStack(spacing: 4) { 314→ Circle() 315→ .fill(latest.index >= 99.9 ? Color.green : Color.orange) 316→ .frame(width: 8, height: 8) 317→ Text(latest.formattedIndex) 318→ .font(.subheadline) 319→ .fontWeight(.semibold) 320→ } 321→ } 322→ } 323→ 324→ if data.isEmpty { 325→ VStack(spacing: 12) { 326→ Image(systemName: "exclamationmark.triangle") 327→ .font(.largeTitle) 328→ .foregroundStyle(.orange) 329→ Text("Dados temporariamente indisponíveis") 330→ .font(.subheadline) 331→ .foregroundStyle(.secondary) 332→ Text("O endpoint do BCB está retornando erro") 333→ .font(.caption) 334→ .foregroundStyle(.tertiary) 335→ } 336→ .frame(maxWidth: .infinity) 337→ .padding(.vertical, 40) 338→ } else { 339→ Chart { 340→ ForEach(sortedData) { item in 341→ BarMark( 342→ x: .value("Mês", item.formattedDate), 343→ y: .value("Índice", item.index) 344→ ) 345→ .foregroundStyle(item.index >= 99.9 ? Color.green : Color.orange) 346→ } 347→ 348→ RuleMark(y: .value("Mínimo", 99.9)) 349→ .foregroundStyle(.red) 350→ .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5])) 351→ .annotation(position: .trailing) { 352→ Text("Mín: 99.9%") 353→ .font(.caption2) 354→ .foregroundStyle(.red) 355→ } 356→ } 357→ .chartYScale(domain: 99.5...100.1) 358→ .chartYAxis { 359→ AxisMarks(values: [99.5, 99.7, 99.9, 100.0]) { value in 360→ AxisGridLine() 361→ AxisValueLabel { 362→ if let v = value.as(Double.self) { 363→ Text(String(format: "%.1f%%", v)) 364→ } 365→ } 366→ } 367→ } 368→ } 369→ 370→ HStack { 371→ Image(systemName: "info.circle") 372→ .foregroundStyle(.secondary) 373→ Text("O SPI deve manter disponibilidade mínima de 99,9%") 374→ .font(.caption) 375→ .foregroundStyle(.secondary) 376→ } 377→ } 378→ .padding() 379→ .frame(maxWidth: .infinity) 380→ .background(Color(NSColor.controlBackgroundColor)) 381→ .clipShape(RoundedRectangle(cornerRadius: 12)) 382→ } 383→} 384→ 385→// MARK: - Remuneration Card 386→ 387→struct PIXRemunerationCard: View { 388→ let data: [PIXRemuneration] 389→ 390→ var latest: PIXRemuneration? { 391→ data.first 392→ } 393→ 394→ var body: some View { 395→ VStack(alignment: .leading, spacing: 16) { 396→ HStack { 397→ Text("Remuneração Conta PI") 398→ .font(.headline) 399→ 400→ Spacer() 401→ 402→ Image(systemName: "percent") 403→ .foregroundStyle(.secondary) 404→ } 405→ 406→ if let item = latest { 407→ VStack(spacing: 16) { 408→ HStack { 409→ VStack(alignment: .leading, spacing: 4) { 410→ Text("Base Total") 411→ .font(.caption) 412→ .foregroundStyle(.secondary) 413→ Text(item.formattedBaseTotal) 414→ .font(.title2) 415→ .fontWeight(.bold) 416→ } 417→ 418→ Spacer() 419→ 420→ VStack(alignment: .trailing, spacing: 4) { 421→ Text("Taxa SELIC") 422→ .font(.caption) 423→ .foregroundStyle(.secondary) 424→ Text(item.formattedSelic) 425→ .font(.title2) 426→ .fontWeight(.bold) 427→ .foregroundStyle(Color.bcbBlue) 428→ } 429→ } 430→ 431→ Divider() 432→ 433→ HStack { 434→ VStack(alignment: .leading, spacing: 4) { 435→ Text("Valor Remunerado (dia)") 436→ .font(.caption) 437→ .foregroundStyle(.secondary) 438→ 439→ let millions = item.remuneratedTotal / 1_000_000 440→ Text(String(format: "R$ %.2f M", millions)) 441→ .font(.headline) 442→ .foregroundStyle(.green) 443→ } 444→ 445→ Spacer() 446→ 447→ VStack(alignment: .trailing, spacing: 4) { 448→ Text("Data Base") 449→ .font(.caption) 450→ .foregroundStyle(.secondary) 451→ Text(item.date) 452→ .font(.subheadline) 453→ } 454→ } 455→ } 456→ } else { 457→ VStack(spacing: 12) { 458→ Image(systemName: "exclamationmark.triangle") 459→ .font(.largeTitle) 460→ .foregroundStyle(.orange) 461→ Text("Dados temporariamente indisponíveis") 462→ .font(.subheadline) 463→ .foregroundStyle(.secondary) 464→ Text("O endpoint do BCB está retornando erro") 465→ .font(.caption) 466→ .foregroundStyle(.tertiary) 467→ } 468→ .frame(maxWidth: .infinity) 469→ .padding(.vertical, 40) 470→ } 471→ 472→ HStack { 473→ Image(systemName: "info.circle") 474→ .foregroundStyle(.secondary) 475→ Text("Recursos mantidos na Conta PI são remunerados pela SELIC") 476→ .font(.caption) 477→ .foregroundStyle(.secondary) 478→ } 479→ } 480→ .padding() 481→ .frame(maxWidth: .infinity) 482→ .background(Color(NSColor.controlBackgroundColor)) 483→ .clipShape(RoundedRectangle(cornerRadius: 12)) 484→ } 485→} 486→ 487→// MARK: - Recent Table 488→ 489→struct PIXRecentTable: View { 490→ let data: [PIXDaily] 491→ 492→ var sortedData: [PIXDaily] { 493→ data.sorted { ($0.parsedDate ?? Date.distantPast) > ($1.parsedDate ?? Date.distantPast) } 494→ .prefix(10) 495→ .map { $0 } 496→ } 497→ 498→ var body: some View { 499→ VStack(alignment: .leading, spacing: 12) { 500→ HStack { 501→ Text("Histórico Recente") 502→ .font(.headline) 503→ 504→ Spacer() 505→ 506→ Text("\(data.count) registros") 507→ .font(.caption) 508→ .foregroundStyle(.secondary) 509→ } 510→ 511→ if data.isEmpty { 512→ ContentUnavailableView("Carregando dados...", systemImage: "table") 513→ } else { 514→ // Table Header 515→ HStack { 516→ Text("Data") 517→ .frame(width: 100, alignment: .leading) 518→ Text("Transações") 519→ .frame(width: 100, alignment: .trailing) 520→ Text("Volume (R$ mil)") 521→ .frame(width: 120, alignment: .trailing) 522→ Text("Ticket Médio") 523→ .frame(width: 100, alignment: .trailing) 524→ Text("Canal Primário") 525→ .frame(width: 120, alignment: .trailing) 526→ Spacer() 527→ } 528→ .font(.caption) 529→ .foregroundStyle(.secondary) 530→ .padding(.horizontal, 8) 531→ 532→ Divider() 533→ 534→ ForEach(sortedData) { item in 535→ HStack { 536→ Text(item.formattedDate) 537→ .frame(width: 100, alignment: .leading) 538→ 539→ Text(item.formattedQuantity) 540→ .frame(width: 100, alignment: .trailing) 541→ .monospacedDigit() 542→ 543→ Text(item.formattedTotal) 544→ .frame(width: 120, alignment: .trailing) 545→ .monospacedDigit() 546→ 547→ Text(item.formattedAverage) 548→ .frame(width: 100, alignment: .trailing) 549→ .monospacedDigit() 550→ 551→ if let primary = item.primaryChannel { 552→ Text(String(format: "%.1f M", Double(primary) / 1_000_000)) 553→ .frame(width: 120, alignment: .trailing) 554→ .monospacedDigit() 555→ } else { 556→ Text("--") 557→ .frame(width: 120, alignment: .trailing) 558→ .foregroundStyle(.secondary) 559→ } 560→ 561→ Spacer() 562→ } 563→ .font(.subheadline) 564→ .padding(.horizontal, 8) 565→ .padding(.vertical, 6) 566→ .background(sortedData.firstIndex(where: { $0.id == item.id })! % 2 == 0 ? Color.clear : Color(NSColor.controlBackgroundColor).opacity(0.5)) 567→ } 568→ } 569→ } 570→ .padding() 571→ .background(Color(NSColor.controlBackgroundColor)) 572→ .clipShape(RoundedRectangle(cornerRadius: 12)) 573→ } 574→} 575→ 576→#Preview { 577→ PIXStatsView() 578→ .environmentObject(DashboardViewModel()) 579→ .frame(width: 1100, height: 900) 580→} 581→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.