diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataCache.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataCache.kt new file mode 100644 index 0000000..f4feb6f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataCache.kt @@ -0,0 +1,210 @@ +package com.mycrib.cache + +import com.mycrib.shared.models.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Centralized data cache for the application. + * This singleton holds all frequently accessed data in memory to avoid redundant API calls. + */ +object DataCache { + + // User & Authentication + private val _currentUser = MutableStateFlow(null) + val currentUser: StateFlow = _currentUser.asStateFlow() + + // Residences + private val _residences = MutableStateFlow>(emptyList()) + val residences: StateFlow> = _residences.asStateFlow() + + private val _myResidences = MutableStateFlow(null) + val myResidences: StateFlow = _myResidences.asStateFlow() + + private val _residenceSummaries = MutableStateFlow>(emptyMap()) + val residenceSummaries: StateFlow> = _residenceSummaries.asStateFlow() + + // Tasks + private val _allTasks = MutableStateFlow(null) + val allTasks: StateFlow = _allTasks.asStateFlow() + + private val _tasksByResidence = MutableStateFlow>(emptyMap()) + val tasksByResidence: StateFlow> = _tasksByResidence.asStateFlow() + + // Documents + private val _documents = MutableStateFlow>(emptyList()) + val documents: StateFlow> = _documents.asStateFlow() + + private val _documentsByResidence = MutableStateFlow>>(emptyMap()) + val documentsByResidence: StateFlow>> = _documentsByResidence.asStateFlow() + + // Contractors + private val _contractors = MutableStateFlow>(emptyList()) + val contractors: StateFlow> = _contractors.asStateFlow() + + // Lookups/Reference Data + private val _categories = MutableStateFlow>(emptyList()) + val categories: StateFlow> = _categories.asStateFlow() + + private val _priorities = MutableStateFlow>(emptyList()) + val priorities: StateFlow> = _priorities.asStateFlow() + + private val _frequencies = MutableStateFlow>(emptyList()) + val frequencies: StateFlow> = _frequencies.asStateFlow() + + private val _statuses = MutableStateFlow>(emptyList()) + val statuses: StateFlow> = _statuses.asStateFlow() + + // Cache metadata + private val _lastRefreshTime = MutableStateFlow(0L) + val lastRefreshTime: StateFlow = _lastRefreshTime.asStateFlow() + + private val _isCacheInitialized = MutableStateFlow(false) + val isCacheInitialized: StateFlow = _isCacheInitialized.asStateFlow() + + // Update methods + fun updateCurrentUser(user: User?) { + _currentUser.value = user + } + + fun updateResidences(residences: List) { + _residences.value = residences + updateLastRefreshTime() + } + + fun updateMyResidences(myResidences: MyResidencesResponse) { + _myResidences.value = myResidences + updateLastRefreshTime() + } + + fun updateResidenceSummary(residenceId: Int, summary: ResidenceSummaryResponse) { + _residenceSummaries.value = _residenceSummaries.value + (residenceId to summary) + } + + fun updateAllTasks(tasks: TaskColumnsResponse) { + _allTasks.value = tasks + updateLastRefreshTime() + } + + fun updateTasksByResidence(residenceId: Int, tasks: TaskColumnsResponse) { + _tasksByResidence.value = _tasksByResidence.value + (residenceId to tasks) + } + + fun updateDocuments(documents: List) { + _documents.value = documents + updateLastRefreshTime() + } + + fun updateDocumentsByResidence(residenceId: Int, documents: List) { + _documentsByResidence.value = _documentsByResidence.value + (residenceId to documents) + } + + fun updateContractors(contractors: List) { + _contractors.value = contractors + updateLastRefreshTime() + } + + fun updateCategories(categories: List) { + _categories.value = categories + } + + fun updatePriorities(priorities: List) { + _priorities.value = priorities + } + + fun updateFrequencies(frequencies: List) { + _frequencies.value = frequencies + } + + fun updateStatuses(statuses: List) { + _statuses.value = statuses + } + + fun setCacheInitialized(initialized: Boolean) { + _isCacheInitialized.value = initialized + } + + private fun updateLastRefreshTime() { + _lastRefreshTime.value = System.currentTimeMillis() + } + + // Helper methods to add/update/remove individual items + fun addResidence(residence: Residence) { + _residences.value = _residences.value + residence + } + + fun updateResidence(residence: Residence) { + _residences.value = _residences.value.map { + if (it.id == residence.id) residence else it + } + } + + fun removeResidence(residenceId: Int) { + _residences.value = _residences.value.filter { it.id != residenceId } + // Also clear related caches + _tasksByResidence.value = _tasksByResidence.value - residenceId + _documentsByResidence.value = _documentsByResidence.value - residenceId + _residenceSummaries.value = _residenceSummaries.value - residenceId + } + + fun addDocument(document: Document) { + _documents.value = _documents.value + document + } + + fun updateDocument(document: Document) { + _documents.value = _documents.value.map { + if (it.id == document.id) document else it + } + } + + fun removeDocument(documentId: Int) { + _documents.value = _documents.value.filter { it.id != documentId } + } + + fun addContractor(contractor: Contractor) { + _contractors.value = _contractors.value + contractor + } + + fun updateContractor(contractor: Contractor) { + _contractors.value = _contractors.value.map { + if (it.id == contractor.id) contractor else it + } + } + + fun removeContractor(contractorId: Int) { + _contractors.value = _contractors.value.filter { it.id != contractorId } + } + + // Clear methods + fun clearAll() { + _currentUser.value = null + _residences.value = emptyList() + _myResidences.value = null + _residenceSummaries.value = emptyMap() + _allTasks.value = null + _tasksByResidence.value = emptyMap() + _documents.value = emptyList() + _documentsByResidence.value = emptyMap() + _contractors.value = emptyList() + _categories.value = emptyList() + _priorities.value = emptyList() + _frequencies.value = emptyList() + _statuses.value = emptyList() + _lastRefreshTime.value = 0L + _isCacheInitialized.value = false + } + + fun clearUserData() { + _currentUser.value = null + _residences.value = emptyList() + _myResidences.value = null + _residenceSummaries.value = emptyMap() + _allTasks.value = null + _tasksByResidence.value = emptyMap() + _documents.value = emptyList() + _documentsByResidence.value = emptyMap() + _contractors.value = emptyList() + _isCacheInitialized.value = false + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt new file mode 100644 index 0000000..0c59e61 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/DataPrefetchManager.kt @@ -0,0 +1,237 @@ +package com.mycrib.cache + +import com.mycrib.shared.network.* +import com.mycrib.storage.TokenStorage +import kotlinx.coroutines.* + +/** + * Manager responsible for prefetching and caching data when the app launches. + * This ensures all screens have immediate access to data without making API calls. + */ +class DataPrefetchManager { + + private val residenceApi = ResidenceApi() + private val taskApi = TaskApi() + private val documentApi = DocumentApi() + private val contractorApi = ContractorApi() + private val lookupsApi = LookupsApi() + + /** + * Prefetch all essential data on app launch. + * This runs asynchronously and populates the DataCache. + */ + suspend fun prefetchAllData(): Result = withContext(Dispatchers.Default) { + try { + val token = TokenStorage.getToken() + if (token == null) { + return@withContext Result.failure(Exception("Not authenticated")) + } + + println("DataPrefetchManager: Starting data prefetch...") + + // Launch all prefetch operations in parallel + val jobs = listOf( + async { prefetchResidences(token) }, + async { prefetchMyResidences(token) }, + async { prefetchTasks(token) }, + async { prefetchDocuments(token) }, + async { prefetchContractors(token) }, + async { prefetchLookups(token) } + ) + + // Wait for all jobs to complete + jobs.awaitAll() + + // Mark cache as initialized + DataCache.setCacheInitialized(true) + + println("DataPrefetchManager: Data prefetch completed successfully") + Result.success(Unit) + } catch (e: Exception) { + println("DataPrefetchManager: Error during prefetch: ${e.message}") + e.printStackTrace() + Result.failure(e) + } + } + + /** + * Refresh specific data types. + * Useful for pull-to-refresh functionality. + */ + suspend fun refreshResidences(): Result = withContext(Dispatchers.Default) { + try { + val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated")) + prefetchResidences(token) + prefetchMyResidences(token) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun refreshTasks(): Result = withContext(Dispatchers.Default) { + try { + val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated")) + prefetchTasks(token) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun refreshDocuments(): Result = withContext(Dispatchers.Default) { + try { + val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated")) + prefetchDocuments(token) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun refreshContractors(): Result = withContext(Dispatchers.Default) { + try { + val token = TokenStorage.getToken() ?: return@withContext Result.failure(Exception("Not authenticated")) + prefetchContractors(token) + Result.success(Unit) + } catch (e: Exception) { + Result.failure(e) + } + } + + // Private prefetch methods + private suspend fun prefetchResidences(token: String) { + try { + println("DataPrefetchManager: Fetching residences...") + val result = residenceApi.getResidences(token) + if (result is ApiResult.Success) { + DataCache.updateResidences(result.data) + println("DataPrefetchManager: Cached ${result.data.size} residences") + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching residences: ${e.message}") + } + } + + private suspend fun prefetchMyResidences(token: String) { + try { + println("DataPrefetchManager: Fetching my residences...") + val result = residenceApi.getMyResidences(token) + if (result is ApiResult.Success) { + DataCache.updateMyResidences(result.data) + println("DataPrefetchManager: Cached my residences") + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching my residences: ${e.message}") + } + } + + private suspend fun prefetchTasks(token: String) { + try { + println("DataPrefetchManager: Fetching tasks...") + val result = taskApi.getTasks(token) + if (result is ApiResult.Success) { + DataCache.updateAllTasks(result.data) + println("DataPrefetchManager: Cached tasks") + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching tasks: ${e.message}") + } + } + + private suspend fun prefetchDocuments(token: String) { + try { + println("DataPrefetchManager: Fetching documents...") + val result = documentApi.getDocuments( + token = token, + residenceId = null, + documentType = null, + category = null, + contractorId = null, + isActive = null, + expiringSoon = null, + tags = null, + search = null + ) + if (result is ApiResult.Success) { + DataCache.updateDocuments(result.data.documents) + println("DataPrefetchManager: Cached ${result.data.documents.size} documents") + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching documents: ${e.message}") + } + } + + private suspend fun prefetchContractors(token: String) { + try { + println("DataPrefetchManager: Fetching contractors...") + val result = contractorApi.getContractors( + token = token, + specialty = null, + isFavorite = null, + isActive = null, + search = null + ) + if (result is ApiResult.Success) { + DataCache.updateContractors(result.data.contractors) + println("DataPrefetchManager: Cached ${result.data.contractors.size} contractors") + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching contractors: ${e.message}") + } + } + + private suspend fun prefetchLookups(token: String) { + try { + println("DataPrefetchManager: Fetching lookups...") + + // Fetch all lookup data in parallel + coroutineScope { + launch { + val result = lookupsApi.getCategories(token) + if (result is ApiResult.Success) { + DataCache.updateCategories(result.data) + println("DataPrefetchManager: Cached ${result.data.size} categories") + } + } + + launch { + val result = lookupsApi.getPriorities(token) + if (result is ApiResult.Success) { + DataCache.updatePriorities(result.data) + println("DataPrefetchManager: Cached ${result.data.size} priorities") + } + } + + launch { + val result = lookupsApi.getFrequencies(token) + if (result is ApiResult.Success) { + DataCache.updateFrequencies(result.data) + println("DataPrefetchManager: Cached ${result.data.size} frequencies") + } + } + + launch { + val result = lookupsApi.getStatuses(token) + if (result is ApiResult.Success) { + DataCache.updateStatuses(result.data) + println("DataPrefetchManager: Cached ${result.data.size} statuses") + } + } + } + } catch (e: Exception) { + println("DataPrefetchManager: Error fetching lookups: ${e.message}") + } + } + + companion object { + private var instance: DataPrefetchManager? = null + + fun getInstance(): DataPrefetchManager { + if (instance == null) { + instance = DataPrefetchManager() + } + return instance!! + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/README_CACHING.md b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/README_CACHING.md new file mode 100644 index 0000000..4bb77df --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/cache/README_CACHING.md @@ -0,0 +1,159 @@ +# Data Caching Implementation + +## Overview + +This app now uses a centralized caching system to avoid redundant API calls when navigating between screens. + +## How It Works + +1. **App Launch**: When the app launches and the user is authenticated, `DataPrefetchManager` automatically loads all essential data in parallel: + - Residences (all + my residences) + - Tasks (all tasks) + - Documents + - Contractors + - Lookup data (categories, priorities, frequencies, statuses) + +2. **Data Access**: ViewModels check the `DataCache` first before making API calls: + - If cache has data and `forceRefresh=false`: Use cached data immediately + - If cache is empty or `forceRefresh=true`: Fetch from API and update cache + +3. **Cache Updates**: When create/update/delete operations succeed, the cache is automatically updated + +## Usage in ViewModels + +### Load Data (with caching) +```kotlin +// In ViewModel +fun loadResidences(forceRefresh: Boolean = false) { + viewModelScope.launch { + // Check cache first + val cachedData = DataCache.residences.value + if (!forceRefresh && cachedData.isNotEmpty()) { + _residencesState.value = ApiResult.Success(cachedData) + return@launch + } + + // Fetch from API if needed + _residencesState.value = ApiResult.Loading + val result = residenceApi.getResidences(token) + _residencesState.value = result + + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateResidences(result.data) + } + } +} +``` + +### Update Cache After Mutations +```kotlin +fun createResidence(request: ResidenceCreateRequest) { + viewModelScope.launch { + val result = residenceApi.createResidence(token, request) + _createState.value = result + + // Update cache on success + if (result is ApiResult.Success) { + DataCache.addResidence(result.data) + } + } +} +``` + +## iOS Integration + +In your iOS app's main initialization (e.g., `iOSApp.swift`): + +```swift +import ComposeApp + +@main +struct MyCribApp: App { + init() { + // After successful login, prefetch data + Task { + await prefetchData() + } + } + + func prefetchData() async { + let prefetchManager = DataPrefetchManager.Companion().getInstance() + _ = try? await prefetchManager.prefetchAllData() + } + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +## Android Integration + +In your Android `MainActivity`: + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Check if authenticated + val token = TokenStorage.getToken() + if (token != null) { + // Prefetch data in background + lifecycleScope.launch { + DataPrefetchManager.getInstance().prefetchAllData() + } + } + + setContent { + // Your compose content + } + } +} +``` + +## Pull to Refresh + +To refresh data manually (e.g., pull-to-refresh): + +```kotlin +// In ViewModel +fun refresh() { + viewModelScope.launch { + prefetchManager.refreshResidences() + loadResidences(forceRefresh = true) + } +} +``` + +## Benefits + +1. **Instant Screen Load**: Screens show data immediately from cache +2. **Reduced API Calls**: No redundant calls when navigating between screens +3. **Better UX**: No loading spinners on every screen transition +4. **Offline Support**: Data remains available even with poor connectivity +5. **Consistent State**: All screens see the same data from cache + +## Cache Lifecycle + +- **Initialization**: App launch (after authentication) +- **Updates**: After successful create/update/delete operations +- **Clear**: On logout or authentication error +- **Refresh**: Manual pull-to-refresh or `forceRefresh=true` + +## ViewModels Updated + +The following ViewModels now use caching: + +- ✅ `ResidenceViewModel` +- ✅ `TaskViewModel` +- ⏳ `DocumentViewModel` (TODO) +- ⏳ `ContractorViewModel` (TODO) +- ⏳ `LookupsViewModel` (TODO) + +## Note + +For DocumentViewModel and ContractorViewModel, follow the same pattern as shown in ResidenceViewModel and TaskViewModel. diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt index 35a4f2d..0b98770 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt @@ -2,6 +2,8 @@ package com.mycrib.android.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.mycrib.cache.DataCache +import com.mycrib.cache.DataPrefetchManager import com.mycrib.shared.models.Residence import com.mycrib.shared.models.ResidenceCreateRequest import com.mycrib.shared.models.ResidenceSummaryResponse @@ -18,6 +20,7 @@ import kotlinx.coroutines.launch class ResidenceViewModel : ViewModel() { private val residenceApi = ResidenceApi() private val taskApi = TaskApi() + private val prefetchManager = DataPrefetchManager.getInstance() private val _residencesState = MutableStateFlow>>(ApiResult.Idle) val residencesState: StateFlow>> = _residencesState @@ -52,12 +55,30 @@ class ResidenceViewModel : ViewModel() { private val _deleteResidenceState = MutableStateFlow>(ApiResult.Idle) val deleteResidenceState: StateFlow> = _deleteResidenceState - fun loadResidences() { + /** + * Load residences from cache. If cache is empty or force refresh is requested, + * fetch from API and update cache. + */ + fun loadResidences(forceRefresh: Boolean = false) { viewModelScope.launch { + // Check if cache is initialized and we have data + val cachedResidences = DataCache.residences.value + if (!forceRefresh && cachedResidences.isNotEmpty()) { + // Use cached data + _residencesState.value = ApiResult.Success(cachedResidences) + return@launch + } + + // Fetch from API _residencesState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _residencesState.value = residenceApi.getResidences(token) + val result = residenceApi.getResidences(token) + _residencesState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateResidences(result.data) + } } else { _residencesState.value = ApiResult.Error("Not authenticated", 401) } @@ -93,7 +114,12 @@ class ResidenceViewModel : ViewModel() { _createResidenceState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _createResidenceState.value = residenceApi.createResidence(token, request) + val result = residenceApi.createResidence(token, request) + _createResidenceState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.addResidence(result.data) + } } else { _createResidenceState.value = ApiResult.Error("Not authenticated", 401) } @@ -121,7 +147,12 @@ class ResidenceViewModel : ViewModel() { _updateResidenceState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _updateResidenceState.value = residenceApi.updateResidence(token, residenceId, request) + val result = residenceApi.updateResidence(token, residenceId, request) + _updateResidenceState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateResidence(result.data) + } } else { _updateResidenceState.value = ApiResult.Error("Not authenticated", 401) } @@ -136,12 +167,24 @@ class ResidenceViewModel : ViewModel() { _updateResidenceState.value = ApiResult.Idle } - fun loadMyResidences() { + fun loadMyResidences(forceRefresh: Boolean = false) { viewModelScope.launch { + // Check cache first + val cachedData = DataCache.myResidences.value + if (!forceRefresh && cachedData != null) { + _myResidencesState.value = ApiResult.Success(cachedData) + return@launch + } + _myResidencesState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _myResidencesState.value = residenceApi.getMyResidences(token) + val result = residenceApi.getMyResidences(token) + _myResidencesState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateMyResidences(result.data) + } } else { _myResidencesState.value = ApiResult.Error("Not authenticated", 401) } @@ -217,7 +260,12 @@ class ResidenceViewModel : ViewModel() { _deleteResidenceState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _deleteResidenceState.value = residenceApi.deleteResidence(token, residenceId) + val result = residenceApi.deleteResidence(token, residenceId) + _deleteResidenceState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.removeResidence(residenceId) + } } else { _deleteResidenceState.value = ApiResult.Error("Not authenticated", 401) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt index 281fa8a..687c859 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt @@ -2,6 +2,8 @@ package com.mycrib.android.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.mycrib.cache.DataCache +import com.mycrib.cache.DataPrefetchManager import com.mycrib.shared.models.TaskColumnsResponse import com.mycrib.shared.models.CustomTask import com.mycrib.shared.models.TaskCreateRequest @@ -14,6 +16,7 @@ import kotlinx.coroutines.launch class TaskViewModel : ViewModel() { private val taskApi = TaskApi() + private val prefetchManager = DataPrefetchManager.getInstance() private val _tasksState = MutableStateFlow>(ApiResult.Idle) val tasksState: StateFlow> = _tasksState @@ -24,27 +27,51 @@ class TaskViewModel : ViewModel() { private val _taskAddNewCustomTaskState = MutableStateFlow>(ApiResult.Idle) val taskAddNewCustomTaskState: StateFlow> = _taskAddNewCustomTaskState - fun loadTasks() { + fun loadTasks(forceRefresh: Boolean = false) { println("TaskViewModel: loadTasks called") viewModelScope.launch { + // Check cache first + val cachedTasks = DataCache.allTasks.value + if (!forceRefresh && cachedTasks != null) { + println("TaskViewModel: Using cached tasks") + _tasksState.value = ApiResult.Success(cachedTasks) + return@launch + } + _tasksState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { val result = taskApi.getTasks(token) println("TaskViewModel: loadTasks result: $result") _tasksState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateAllTasks(result.data) + } } else { _tasksState.value = ApiResult.Error("Not authenticated", 401) } } } - fun loadTasksByResidence(residenceId: Int) { + fun loadTasksByResidence(residenceId: Int, forceRefresh: Boolean = false) { viewModelScope.launch { + // Check cache first + val cachedTasks = DataCache.tasksByResidence.value[residenceId] + if (!forceRefresh && cachedTasks != null) { + _tasksByResidenceState.value = ApiResult.Success(cachedTasks) + return@launch + } + _tasksByResidenceState.value = ApiResult.Loading val token = TokenStorage.getToken() if (token != null) { - _tasksByResidenceState.value = taskApi.getTasksByResidence(token, residenceId) + val result = taskApi.getTasksByResidence(token, residenceId) + _tasksByResidenceState.value = result + // Update cache on success + if (result is ApiResult.Success) { + DataCache.updateTasksByResidence(residenceId, result.data) + } } else { _tasksByResidenceState.value = ApiResult.Error("Not authenticated", 401) } diff --git a/iosApp/iosApp/Login/LoginViewModel.swift b/iosApp/iosApp/Login/LoginViewModel.swift index 0a5cb1d..7a478e6 100644 --- a/iosApp/iosApp/Login/LoginViewModel.swift +++ b/iosApp/iosApp/Login/LoginViewModel.swift @@ -167,6 +167,19 @@ class LoginViewModel: ObservableObject { // Initialize lookups repository after successful login LookupsManager.shared.initialize() + // Prefetch all data for caching + Task { + do { + print("Starting data prefetch...") + let prefetchManager = DataPrefetchManager.Companion().getInstance() + _ = try await prefetchManager.prefetchAllData() + print("Data prefetch completed successfully") + } catch { + print("Data prefetch failed: \(error.localizedDescription)") + // Don't block login on prefetch failure + } + } + // Update authentication state AFTER setting verified status // Small delay to ensure state updates are processed DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -192,6 +205,9 @@ class LoginViewModel: ObservableObject { // Clear lookups data on logout LookupsManager.shared.clear() + // Clear all cached data + DataCache.shared.clearAll() + // Reset state isAuthenticated = false isVerified = false