1→import SwiftUI 2→ 3→enum NavigationItem: String, CaseIterable, Identifiable { 4→ case dashboard = "Dashboard" 5→ case currency = "Câmbio" 6→ case rates = "Taxas" 7→ case institutions = "Instituições" 8→ case pix = "PIX" 9→ case expectations = "Focus" 10→ 11→ var id: String { rawValue } 12→ 13→ var icon: String { 14→ switch self { 15→ case .dashboard: return "square.grid.2x2" 16→ case .currency: return "dollarsign.circle" 17→ case .rates: return "percent" 18→ case .institutions: return "building.columns" 19→ case .pix: return "qrcode" 20→ case .expectations: return "chart.line.uptrend.xyaxis" 21→ } 22→ } 23→ 24→ var color: Color { 25→ switch self { 26→ case .dashboard: return .bcbBlue 27→ case .currency: return .bcbGreen 28→ case .rates: return .bcbPurple 29→ case .institutions: return .bcbPink 30→ case .pix: return .green 31→ case .expectations: return .bcbYellow 32→ } 33→ } 34→} 35→ 36→struct ContentView: View { 37→ @EnvironmentObject var viewModel: DashboardViewModel 38→ @State private var selectedItem: NavigationItem? = .dashboard 39→ @State private var columnVisibility: NavigationSplitViewVisibility = .all 40→ @State private var showingExportSheet = false 41→ @State private var exportFormat: ExportFormat = .json 42→ 43→ var body: some View { 44→ NavigationSplitView(columnVisibility: $columnVisibility) { 45→ SidebarView(selectedItem: $selectedItem) 46→ } detail: { 47→ Group { 48→ switch selectedItem { 49→ case .dashboard: 50→ DashboardView() 51→ case .currency: 52→ CurrencyListView() 53→ case .rates: 54→ RatesView() 55→ case .institutions: 56→ InstitutionsView() 57→ case .pix: 58→ PIXStatsView() 59→ case .expectations: 60→ ExpectationsView() 61→ case .none: 62→ WelcomeView() 63→ } 64→ } 65→ .frame(maxWidth: .infinity, maxHeight: .infinity) 66→ } 67→ .navigationSplitViewStyle(.balanced) 68→ .toolbar { 69→ ToolbarItemGroup(placement: .navigation) { 70→ // Status indicator 71→ HStack(spacing: 6) { 72→ Circle() 73→ .fill(viewModel.errorMessage == nil ? .green : .red) 74→ .frame(width: 8, height: 8) 75→ 76→ if viewModel.isLoading { 77→ Text("Atualizando...") 78→ .font(.caption) 79→ .foregroundStyle(.secondary) 80→ } else if let error = viewModel.errorMessage { 81→ Text("Erro") 82→ .font(.caption) 83→ .foregroundStyle(.red) 84→ .help(error) 85→ } else { 86→ Text("Online") 87→ .font(.caption) 88→ .foregroundStyle(.secondary) 89→ } 90→ } 91→ } 92→ 93→ ToolbarItemGroup(placement: .primaryAction) { 94→ // Loading indicator 95→ if viewModel.isLoading { 96→ ProgressView() 97→ .controlSize(.small) 98→ } 99→ 100→ // Quick values 101→ QuickValuePill( 102→ label: "USD", 103→ value: viewModel.dollarQuote?.sellRate.toBRL() ?? "--", 104→ color: .bcbGreen 105→ ) 106→ 107→ QuickValuePill( 108→ label: "SELIC", 109→ value: String(format: "%.2f%%", viewModel.selicTarget?.numericValue ?? 0.0), 110→ color: .bcbPurple 111→ ) 112→ 113→ Divider() 114→ 115→ // Refresh button 116→ Button { 117→ Task { 118→ await viewModel.refreshAll() 119→ } 120→ } label: { 121→ Image(systemName: "arrow.clockwise") 122→ } 123→ .keyboardShortcut("r", modifiers: .command) 124→ .disabled(viewModel.isLoading) 125→ .help("Atualizar (⌘R)") 126→ 127→ // Export button 128→ Menu { 129→ Button { 130→ exportFormat = .json 131→ exportData() 132→ } label: { 133→ Label("Exportar JSON", systemImage: "doc.text") 134→ } 135→ 136→ Button { 137→ exportFormat = .csv 138→ exportData() 139→ } label: { 140→ Label("Exportar CSV", systemImage: "tablecells") 141→ } 142→ 143→ Divider() 144→ 145→ Button { 146→ copyToClipboard() 147→ } label: { 148→ Label("Copiar Resumo", systemImage: "doc.on.doc") 149→ } 150→ } label: { 151→ Image(systemName: "square.and.arrow.up") 152→ } 153→ .help("Exportar dados") 154→ 155→ // Last update 156→ if let lastUpdate = viewModel.lastUpdate { 157→ Text(lastUpdate.toRelativeString()) 158→ .font(.caption) 159→ .foregroundStyle(.secondary) 160→ .help(lastUpdate.toFullBRString()) 161→ } 162→ } 163→ } 164→ .task { 165→ await viewModel.refreshAll() 166→ } 167→ .alert("Erro", isPresented: .constant(viewModel.errorMessage != nil)) { 168→ Button("OK") { 169→ viewModel.dismissError() 170→ } 171→ Button("Tentar Novamente") { 172→ Task { 173→ await viewModel.refreshAll() 174→ } 175→ } 176→ } message: { 177→ Text(viewModel.errorMessage ?? "Erro desconhecido") 178→ } 179→ } 180→ 181→ private func exportData() { 182→ let panel = NSSavePanel() 183→ panel.allowedContentTypes = exportFormat == .json ? [.json] : [.commaSeparatedText] 184→ panel.nameFieldStringValue = "bcb-data-\(Date().toFilenameDateString()).\(exportFormat.rawValue)" 185→ 186→ panel.begin { response in 187→ if response == .OK, let url = panel.url { 188→ let data = generateExportData(format: exportFormat) 189→ try? data.write(to: url, atomically: true, encoding: .utf8) 190→ } 191→ } 192→ } 193→ 194→ private func generateExportData(format: ExportFormat) -> String { 195→ switch format { 196→ case .json: 197→ return generateJSONExport() 198→ case .csv: 199→ return generateCSVExport() 200→ } 201→ } 202→ 203→ private func generateJSONExport() -> String { 204→ var export: [String: Any] = [:] 205→ 206→ export["exportDate"] = Date().toFullBRString() 207→ export["source"] = "Banco Central do Brasil" 208→ 209→ if let dollar = viewModel.dollarQuote { 210→ export["dollarQuote"] = [ 211→ "buy": dollar.buyRate, 212→ "sell": dollar.sellRate, 213→ "date": dollar.formattedDate 214→ ] 215→ } 216→ 217→ if let euro = viewModel.euroQuote { 218→ export["euroQuote"] = [ 219→ "buy": euro.buyRate, 220→ "sell": euro.sellRate, 221→ "date": euro.formattedDate 222→ ] 223→ } 224→ 225→ var rates: [String: Double] = [:] 226→ rates["selicTarget"] = viewModel.selicTarget?.numericValue ?? 0 227→ rates["selicDaily"] = viewModel.selicDaily?.numericValue ?? 0 228→ rates["cdi"] = viewModel.cdi?.numericValue ?? 0 229→ rates["ipcaMonthly"] = viewModel.ipcaMonthly?.numericValue ?? 0 230→ rates["ipca12m"] = viewModel.ipca12m?.numericValue ?? 0 231→ export["rates"] = rates 232→ 233→ if let selicExp = viewModel.focusStats.latestSelic { 234→ export["focusExpectations"] = [ 235→ "selicExpected": selicExp.median, 236→ "selicMeeting": selicExp.meeting 237→ ] 238→ } 239→ 240→ if let jsonData = try? JSONSerialization.data(withJSONObject: export, options: .prettyPrinted), 241→ let jsonString = String(data: jsonData, encoding: .utf8) { 242→ return jsonString 243→ } 244→ return "{}" 245→ } 246→ 247→ private func generateCSVExport() -> String { 248→ var csv = "Indicador,Valor,Data\n" 249→ 250→ if let dollar = viewModel.dollarQuote { 251→ csv += "Dólar (Compra),\(dollar.buyRate),\(dollar.formattedDate)\n" 252→ csv += "Dólar (Venda),\(dollar.sellRate),\(dollar.formattedDate)\n" 253→ } 254→ 255→ if let euro = viewModel.euroQuote { 256→ csv += "Euro (Compra),\(euro.buyRate),\(euro.formattedDate)\n" 257→ csv += "Euro (Venda),\(euro.sellRate),\(euro.formattedDate)\n" 258→ } 259→ 260→ if let selic = viewModel.selicTarget { 261→ csv += "SELIC Meta,\(selic.numericValue),\(selic.date)\n" 262→ } 263→ 264→ if let cdi = viewModel.cdi { 265→ csv += "CDI,\(cdi.numericValue),\(cdi.date)\n" 266→ } 267→ 268→ if let ipca = viewModel.ipca12m { 269→ csv += "IPCA 12m,\(ipca.numericValue),\(ipca.date)\n" 270→ } 271→ 272→ return csv 273→ } 274→ 275→ private func copyToClipboard() { 276→ var summary = "📊 BCB Dashboard - \(Date().toFullBRString())\n\n" 277→ 278→ if let dollar = viewModel.dollarQuote { 279→ summary += "💵 Dólar: \(dollar.sellRate.toBRL())\n" 280→ } 281→ 282→ if let euro = viewModel.euroQuote { 283→ summary += "💶 Euro: \(euro.sellRate.toBRL())\n" 284→ } 285→ 286→ if let selic = viewModel.selicTarget { 287→ summary += "📈 SELIC: \(String(format: "%.2f%%", selic.numericValue))\n" 288→ } 289→ 290→ if let ipca = viewModel.ipca12m { 291→ summary += "📉 IPCA 12m: \(String(format: "%.2f%%", ipca.numericValue))\n" 292→ } 293→ 294→ summary += "\nFonte: Banco Central do Brasil" 295→ 296→ NSPasteboard.general.clearContents() 297→ NSPasteboard.general.setString(summary, forType: .string) 298→ } 299→} 300→ 301→enum ExportFormat: String { 302→ case json 303→ case csv 304→} 305→ 306→// MARK: - Quick Value Pill 307→ 308→struct QuickValuePill: View { 309→ let label: String 310→ let value: String 311→ let color: Color 312→ 313→ var body: some View { 314→ HStack(spacing: 4) { 315→ Text(label) 316→ .font(.caption2) 317→ .foregroundStyle(.secondary) 318→ 319→ Text(value) 320→ .font(.caption) 321→ .fontWeight(.semibold) 322→ .foregroundStyle(color) 323→ } 324→ .padding(.horizontal, 8) 325→ .padding(.vertical, 4) 326→ .background(color.opacity(0.1)) 327→ .clipShape(Capsule()) 328→ } 329→} 330→ 331→// MARK: - Welcome View 332→ 333→struct WelcomeView: View { 334→ var body: some View { 335→ VStack(spacing: 20) { 336→ Image(systemName: "building.columns.fill") 337→ .font(.system(size: 64)) 338→ .foregroundStyle(Color.bcbBlue) 339→ 340→ Text("BCB Dashboard") 341→ .font(.largeTitle) 342→ .fontWeight(.bold) 343→ 344→ Text("Selecione um item no menu lateral") 345→ .foregroundStyle(.secondary) 346→ } 347→ } 348→} 349→ 350→// MARK: - Sidebar View 351→ 352→struct SidebarView: View { 353→ @Binding var selectedItem: NavigationItem? 354→ @EnvironmentObject var viewModel: DashboardViewModel 355→ 356→ var body: some View { 357→ List(selection: $selectedItem) { 358→ Section("Overview") { 359→ NavigationLink(value: NavigationItem.dashboard) { 360→ SidebarRow( 361→ icon: "square.grid.2x2", 362→ title: "Dashboard", 363→ color: .bcbBlue, 364→ badge: nil 365→ ) 366→ } 367→ } 368→ 369→ Section("Mercado") { 370→ NavigationLink(value: NavigationItem.currency) { 371→ SidebarRow( 372→ icon: "dollarsign.circle", 373→ title: "Câmbio", 374→ color: .bcbGreen, 375→ badge: viewModel.dollarQuote?.sellRate.toBRL() 376→ ) 377→ } 378→ 379→ NavigationLink(value: NavigationItem.rates) { 380→ SidebarRow( 381→ icon: "percent", 382→ title: "Taxas", 383→ color: .bcbPurple, 384→ badge: viewModel.selicTarget.map { String(format: "%.1f%%", $0.numericValue) } 385→ ) 386→ } 387→ } 388→ 389→ Section("Análise") { 390→ NavigationLink(value: NavigationItem.expectations) { 391→ SidebarRow( 392→ icon: "chart.line.uptrend.xyaxis", 393→ title: "Focus", 394→ color: .bcbYellow, 395→ badge: viewModel.focusStats.latestSelic.map { String(format: "%.1f%%", $0.median) } 396→ ) 397→ } 398→ 399→ NavigationLink(value: NavigationItem.pix) { 400→ SidebarRow( 401→ icon: "qrcode", 402→ title: "PIX", 403→ color: .green, 404→ badge: viewModel.pixStats.latestDaily.map { "\($0.quantity / 1_000_000)M" } 405→ ) 406→ } 407→ } 408→ 409→ Section("Referência") { 410→ NavigationLink(value: NavigationItem.institutions) { 411→ SidebarRow( 412→ icon: "building.columns", 413→ title: "Instituições", 414→ color: .bcbPink, 415→ badge: viewModel.banks.isEmpty ? nil : "\(viewModel.banks.count + viewModel.cooperatives.count)" 416→ ) 417→ } 418→ } 419→ 420→ Section { 421→ // Status Footer 422→ VStack(alignment: .leading, spacing: 8) { 423→ if let lastUpdate = viewModel.lastUpdate { 424→ HStack { 425→ Image(systemName: "clock") 426→ .font(.caption2) 427→ Text(lastUpdate.toRelativeString()) 428→ .font(.caption2) 429→ } 430→ .foregroundStyle(.secondary) 431→ } 432→ 433→ if viewModel.isLoading { 434→ HStack { 435→ ProgressView() 436→ .controlSize(.mini) 437→ Text("Atualizando...") 438→ .font(.caption2) 439→ } 440→ .foregroundStyle(.secondary) 441→ } 442→ } 443→ .padding(.vertical, 4) 444→ } 445→ } 446→ .listStyle(.sidebar) 447→ .navigationTitle("BCB") 448→ .frame(minWidth: 220) 449→ } 450→} 451→ 452→struct SidebarRow: View { 453→ let icon: String 454→ let title: String 455→ let color: Color 456→ let badge: String? 457→ 458→ var body: some View { 459→ HStack { 460→ Image(systemName: icon) 461→ .foregroundStyle(color) 462→ .frame(width: 24) 463→ 464→ Text(title) 465→ 466→ Spacer() 467→ 468→ if let badge = badge { 469→ Text(badge) 470→ .font(.caption2) 471→ .fontWeight(.medium) 472→ .foregroundStyle(color) 473→ .padding(.horizontal, 6) 474→ .padding(.vertical, 2) 475→ .background(color.opacity(0.15)) 476→ .clipShape(Capsule()) 477→ } 478→ } 479→ } 480→} 481→ 482→// MARK: - Date Extension 483→ 484→extension Date { 485→ func toRelativeString() -> String { 486→ let formatter = RelativeDateTimeFormatter() 487→ formatter.unitsStyle = .abbreviated 488→ formatter.locale = Locale(identifier: "pt_BR") 489→ return formatter.localizedString(for: self, relativeTo: Date()) 490→ } 491→ 492→ func toFilenameDateString() -> String { 493→ let formatter = DateFormatter() 494→ formatter.dateFormat = "yyyy-MM-dd-HHmmss" 495→ return formatter.string(from: self) 496→ } 497→} 498→ 499→#Preview { 500→ ContentView() 501→ .environmentObject(DashboardViewModel()) 502→} 503→ 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.