1→import SwiftUI 2→import Combine 3→ 4→@MainActor 5→class DashboardViewModel: ObservableObject { 6→ // MARK: - Published Properties 7→ 8→ @Published var isLoading = false 9→ @Published var errorMessage: String? 10→ @Published var lastUpdate: Date? 11→ 12→ // Currency Data 13→ @Published var dollarQuote: DollarQuote? 14→ @Published var dollarHistory: [DollarQuote] = [] 15→ @Published var euroQuote: CurrencyQuote? 16→ @Published var currencies: [CurrencyInfo] = [] 17→ 18→ // Rates Data 19→ @Published var selicTarget: TimeSeriesValue? 20→ @Published var selicDaily: TimeSeriesValue? 21→ @Published var cdi: TimeSeriesValue? 22→ @Published var ipcaMonthly: TimeSeriesValue? 23→ @Published var ipca12m: TimeSeriesValue? 24→ @Published var selicHistory: [TimeSeriesValue] = [] 25→ @Published var ipcaHistory: [TimeSeriesValue] = [] 26→ 27→ // Institutions (new data structure) 28→ @Published var banks: [Bank] = [] 29→ @Published var cooperatives: [Cooperative] = [] 30→ @Published var consortia: [Consortium] = [] 31→ @Published var sociedades: [Sociedade] = [] 32→ @Published var institutionsLoading = false 33→ @Published var institutionStats = InstitutionStats() 34→ 35→ // Legacy 36→ @Published var institutions: [Institution] = [] 37→ @Published var searchQuery = "" 38→ 39→ // PIX Statistics 40→ @Published var pixStats = PIXStats() 41→ @Published var pixLoading = false 42→ 43→ // Expectations (Focus) 44→ @Published var focusStats = FocusStats() 45→ @Published var focusLoading = false 46→ 47→ // Legacy expectations 48→ @Published var inflationExpectations: [MarketExpectation] = [] 49→ @Published var selicExpectations: [MarketExpectation] = [] 50→ 51→ // Services 52→ private let apiClient = BCBAPIClient.shared 53→ private let cache = CacheManager.shared 54→ private let notificationService = NotificationService.shared 55→ private var refreshTimer: Timer? 56→ 57→ // MARK: - Computed Properties 58→ 59→ var dollarChange: Double { 60→ guard dollarHistory.count >= 2 else { return 0 } 61→ let current = dollarHistory.last?.sellRate ?? 0 62→ let previous = dollarHistory[dollarHistory.count - 2].sellRate 63→ return ((current - previous) / previous) * 100 64→ } 65→ 66→ var realInterestRate: Double { 67→ guard let selic = selicTarget?.numericValue, 68→ let ipca = ipca12m?.numericValue else { return 0 } 69→ return ((1 + selic / 100) / (1 + ipca / 100) - 1) * 100 70→ } 71→ 72→ // MARK: - Initialization 73→ 74→ init() { 75→ setupAutoRefresh() 76→ requestNotificationPermission() 77→ } 78→ 79→ private func requestNotificationPermission() { 80→ Task { 81→ await notificationService.requestAuthorization() 82→ } 83→ } 84→ 85→ deinit { 86→ refreshTimer?.invalidate() 87→ } 88→ 89→ // MARK: - Auto Refresh 90→ 91→ private func setupAutoRefresh() { 92→ // Cancel existing timer 93→ refreshTimer?.invalidate() 94→ refreshTimer = nil 95→ 96→ let interval = UserDefaults.standard.integer(forKey: "refreshInterval") 97→ 98→ // -1 means disabled, skip setup 99→ guard interval >= 0 else { return } 100→ 101→ // Calculate interval in seconds (0 = 30 seconds, otherwise minutes * 60) 102→ let intervalSeconds: TimeInterval = interval == 0 ? 30 : TimeInterval(interval * 60) 103→ 104→ refreshTimer = Timer.scheduledTimer(withTimeInterval: intervalSeconds, repeats: true) { [weak self] _ in 105→ Task { @MainActor in 106→ await self?.refreshAll() 107→ } 108→ } 109→ 110→ // Observe changes to refresh interval 111→ NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { [weak self] _ in 112→ self?.setupAutoRefresh() 113→ } 114→ } 115→ 116→ func updateRefreshInterval() { 117→ setupAutoRefresh() 118→ } 119→ 120→ // MARK: - Data Loading 121→ 122→ func refreshAll() async { 123→ isLoading = true 124→ errorMessage = nil 125→ 126→ async let currencyTask: () = loadCurrencyData() 127→ async let ratesTask: () = loadRatesData() 128→ 129→ await currencyTask 130→ await ratesTask 131→ 132→ lastUpdate = Date() 133→ isLoading = false 134→ } 135→ 136→ func loadCurrencyData() async { 137→ do { 138→ // Try to get today's quote, if not available try previous days 139→ var quotes: [DollarQuote] = [] 140→ var attempts = 0 141→ 142→ while quotes.isEmpty && attempts < 5 { 143→ let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date() 144→ quotes = try await apiClient.fetchDollarQuote(date: date) 145→ attempts += 1 146→ } 147→ 148→ if let quote = quotes.last { 149→ // Check for dollar change and notify 150→ notificationService.checkDollarChange(newRate: quote.sellRate) 151→ dollarQuote = quote 152→ } 153→ 154→ // Get dollar history (last 30 days) 155→ let endDate = Date() 156→ let startDate = Calendar.current.date(byAdding: .day, value: -30, to: endDate) ?? endDate 157→ dollarHistory = try await apiClient.fetchDollarQuotePeriod(startDate: startDate, endDate: endDate) 158→ 159→ // Get Euro quote 160→ attempts = 0 161→ var euroQuotes: [CurrencyQuote] = [] 162→ while euroQuotes.isEmpty && attempts < 5 { 163→ let date = Calendar.current.date(byAdding: .day, value: -attempts, to: Date()) ?? Date() 164→ euroQuotes = try await apiClient.fetchCurrencyQuote(symbol: "EUR", date: date) 165→ attempts += 1 166→ } 167→ 168→ if let quote = euroQuotes.last(where: { $0.isPTAXClosing }) ?? euroQuotes.last { 169→ // Check for euro change and notify 170→ notificationService.checkEuroChange(newRate: quote.sellRate) 171→ euroQuote = quote 172→ } 173→ 174→ // Get available currencies 175→ currencies = try await apiClient.fetchCurrencies() 176→ 177→ } catch { 178→ errorMessage = error.localizedDescription 179→ } 180→ } 181→ 182→ func loadRatesData() async { 183→ do { 184→ // SELIC 185→ async let selicTargetData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 2) 186→ async let selicDailyData = apiClient.fetchTimeSeries(code: SeriesCode.selicDaily.rawValue, lastN: 1) 187→ async let cdiData = apiClient.fetchTimeSeries(code: SeriesCode.cdi.rawValue, lastN: 1) 188→ 189→ // IPCA 190→ async let ipcaMonthlyData = apiClient.fetchTimeSeries(code: SeriesCode.ipcaMonthly.rawValue, lastN: 12) 191→ async let ipca12mData = apiClient.fetchTimeSeries(code: SeriesCode.ipca12m.rawValue, lastN: 2) 192→ 193→ // History 194→ async let selicHistoryData = apiClient.fetchTimeSeries(code: SeriesCode.selicTarget.rawValue, lastN: 20) 195→ 196→ let (selicT, selicD, cdiVal, ipcaM, ipca12, selicH) = try await ( 197→ selicTargetData, selicDailyData, cdiData, ipcaMonthlyData, ipca12mData, selicHistoryData 198→ ) 199→ 200→ selicTarget = selicT.last 201→ selicDaily = selicD.last 202→ cdi = cdiVal.last 203→ ipcaMonthly = ipcaM.last 204→ ipca12m = ipca12.last 205→ ipcaHistory = ipcaM 206→ selicHistory = selicH 207→ 208→ } catch { 209→ errorMessage = error.localizedDescription 210→ } 211→ } 212→ 213→ func loadInstitutions() async { 214→ institutionsLoading = true 215→ defer { institutionsLoading = false } 216→ 217→ do { 218→ let result = try await apiClient.fetchAllInstitutions() 219→ 220→ banks = result.banks.sorted { $0.name < $1.name } 221→ cooperatives = result.cooperatives.sorted { $0.name < $1.name } 222→ consortia = result.consortia.sorted { $0.name < $1.name } 223→ sociedades = result.sociedades.sorted { $0.name < $1.name } 224→ 225→ // Update stats 226→ institutionStats.totalBanks = banks.count 227→ institutionStats.totalCooperatives = cooperatives.count 228→ institutionStats.totalConsortia = consortia.count 229→ institutionStats.totalSociedades = sociedades.count 230→ 231→ // Calculate state distribution 232→ var stateCount: [String: Int] = [:] 233→ for bank in banks { 234→ if let state = bank.state { 235→ stateCount[state, default: 0] += 1 236→ } 237→ } 238→ for coop in cooperatives { 239→ if let state = coop.state { 240→ stateCount[state, default: 0] += 1 241→ } 242→ } 243→ for cons in consortia { 244→ if let state = cons.state { 245→ stateCount[state, default: 0] += 1 246→ } 247→ } 248→ for soc in sociedades { 249→ if let state = soc.state { 250→ stateCount[state, default: 0] += 1 251→ } 252→ } 253→ institutionStats.stateDistribution = stateCount 254→ 255→ } catch { 256→ errorMessage = error.localizedDescription 257→ } 258→ } 259→ 260→ func searchInstitutions() async { 261→ // Legacy method - not used anymore 262→ } 263→ 264→ func loadExpectations() async { 265→ // Legacy method - kept for compatibility 266→ do { 267→ async let inflationData = apiClient.fetchInflationExpectations() 268→ async let selicData = apiClient.fetchSelicExpectations() 269→ 270→ let (inflation, selic) = try await (inflationData, selicData) 271→ inflationExpectations = inflation 272→ selicExpectations = selic 273→ } catch { 274→ errorMessage = error.localizedDescription 275→ } 276→ } 277→ 278→ func loadFocusStats() async { 279→ focusLoading = true 280→ defer { focusLoading = false } 281→ 282→ do { 283→ focusStats = try await apiClient.fetchAllFocusStats() 284→ } catch { 285→ errorMessage = error.localizedDescription 286→ } 287→ } 288→ 289→ func loadPIXStats() async { 290→ pixLoading = true 291→ defer { pixLoading = false } 292→ 293→ do { 294→ pixStats = try await apiClient.fetchAllPIXStats() 295→ } catch { 296→ errorMessage = error.localizedDescription 297→ } 298→ } 299→ 300→ // MARK: - Cache 301→ 302→ func clearCache() { 303→ cache.clearAll() 304→ Task { 305→ await refreshAll() 306→ } 307→ } 308→ 309→ // MARK: - Utility 310→ 311→ func dismissError() { 312→ errorMessage = nil 313→ } 314→} 315→ 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.