package com.example.casera.data import com.example.casera.models.* import com.example.casera.storage.TokenManager import com.example.casera.storage.ThemeStorageManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.time.Clock import kotlin.time.ExperimentalTime /** * Unified DataManager - Single Source of Truth for all app data. * * Core Principles: * 1. All data is cached here - no other caches exist * 2. Every API response updates DataManager immediately * 3. All screens observe DataManager StateFlows directly * 4. All data is persisted to disk for offline access * 5. Includes auth token and theme preferences * * Data Flow: * User Action → API Call → Server Response → DataManager Updated → All Screens React */ object DataManager { // ==================== CACHE CONFIGURATION ==================== /** * Cache timeout in milliseconds. * Data older than this will be refreshed from the API. * Default: 1 hour (3600000ms) */ const val CACHE_TIMEOUT_MS: Long = 60 * 60 * 1000L // 1 hour // Cache timestamps for each data type (epoch milliseconds) var residencesCacheTime: Long = 0L private set var myResidencesCacheTime: Long = 0L private set var tasksCacheTime: Long = 0L private set var tasksByResidenceCacheTime: MutableMap = mutableMapOf() private set var contractorsCacheTime: Long = 0L private set var documentsCacheTime: Long = 0L private set var summaryCacheTime: Long = 0L private set /** * Check if cache for a given timestamp is still valid (not expired) */ @OptIn(ExperimentalTime::class) fun isCacheValid(cacheTime: Long): Boolean { if (cacheTime == 0L) return false val now = Clock.System.now().toEpochMilliseconds() return (now - cacheTime) < CACHE_TIMEOUT_MS } /** * Get current timestamp in milliseconds */ @OptIn(ExperimentalTime::class) private fun currentTimeMs(): Long = Clock.System.now().toEpochMilliseconds() // Platform-specific persistence managers (initialized at app start) private var tokenManager: TokenManager? = null private var themeManager: ThemeStorageManager? = null private var persistenceManager: PersistenceManager? = null private val json = Json { ignoreUnknownKeys = true isLenient = true encodeDefaults = true } // ==================== AUTHENTICATION ==================== private val _authToken = MutableStateFlow(null) val authToken: StateFlow = _authToken.asStateFlow() private val _currentUser = MutableStateFlow(null) val currentUser: StateFlow = _currentUser.asStateFlow() // ==================== APP PREFERENCES ==================== private val _themeId = MutableStateFlow("default") val themeId: StateFlow = _themeId.asStateFlow() // ==================== ONBOARDING ==================== private val _hasCompletedOnboarding = MutableStateFlow(false) val hasCompletedOnboarding: StateFlow = _hasCompletedOnboarding.asStateFlow() // ==================== RESIDENCES ==================== private val _residences = MutableStateFlow>(emptyList()) val residences: StateFlow> = _residences.asStateFlow() private val _myResidences = MutableStateFlow(null) val myResidences: StateFlow = _myResidences.asStateFlow() private val _totalSummary = MutableStateFlow(null) val totalSummary: StateFlow = _totalSummary.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 ==================== // Stores ContractorSummary for list views (lighter weight than full Contractor) private val _contractors = MutableStateFlow>(emptyList()) val contractors: StateFlow> = _contractors.asStateFlow() // ==================== SUBSCRIPTION ==================== private val _subscription = MutableStateFlow(null) val subscription: StateFlow = _subscription.asStateFlow() private val _upgradeTriggers = MutableStateFlow>(emptyMap()) val upgradeTriggers: StateFlow> = _upgradeTriggers.asStateFlow() private val _featureBenefits = MutableStateFlow>(emptyList()) val featureBenefits: StateFlow> = _featureBenefits.asStateFlow() private val _promotions = MutableStateFlow>(emptyList()) val promotions: StateFlow> = _promotions.asStateFlow() // ==================== LOOKUPS (Reference Data) ==================== // List-based for dropdowns/pickers private val _residenceTypes = MutableStateFlow>(emptyList()) val residenceTypes: StateFlow> = _residenceTypes.asStateFlow() private val _taskFrequencies = MutableStateFlow>(emptyList()) val taskFrequencies: StateFlow> = _taskFrequencies.asStateFlow() private val _taskPriorities = MutableStateFlow>(emptyList()) val taskPriorities: StateFlow> = _taskPriorities.asStateFlow() private val _taskCategories = MutableStateFlow>(emptyList()) val taskCategories: StateFlow> = _taskCategories.asStateFlow() private val _contractorSpecialties = MutableStateFlow>(emptyList()) val contractorSpecialties: StateFlow> = _contractorSpecialties.asStateFlow() // ==================== TASK TEMPLATES ==================== private val _taskTemplates = MutableStateFlow>(emptyList()) val taskTemplates: StateFlow> = _taskTemplates.asStateFlow() private val _taskTemplatesGrouped = MutableStateFlow(null) val taskTemplatesGrouped: StateFlow = _taskTemplatesGrouped.asStateFlow() // Map-based for O(1) ID resolution private val _residenceTypesMap = MutableStateFlow>(emptyMap()) val residenceTypesMap: StateFlow> = _residenceTypesMap.asStateFlow() private val _taskFrequenciesMap = MutableStateFlow>(emptyMap()) val taskFrequenciesMap: StateFlow> = _taskFrequenciesMap.asStateFlow() private val _taskPrioritiesMap = MutableStateFlow>(emptyMap()) val taskPrioritiesMap: StateFlow> = _taskPrioritiesMap.asStateFlow() private val _taskCategoriesMap = MutableStateFlow>(emptyMap()) val taskCategoriesMap: StateFlow> = _taskCategoriesMap.asStateFlow() private val _contractorSpecialtiesMap = MutableStateFlow>(emptyMap()) val contractorSpecialtiesMap: StateFlow> = _contractorSpecialtiesMap.asStateFlow() // ==================== STATE METADATA ==================== private val _isInitialized = MutableStateFlow(false) val isInitialized: StateFlow = _isInitialized.asStateFlow() private val _lookupsInitialized = MutableStateFlow(false) val lookupsInitialized: StateFlow = _lookupsInitialized.asStateFlow() private val _lastSyncTime = MutableStateFlow(0L) val lastSyncTime: StateFlow = _lastSyncTime.asStateFlow() // ==================== SEEDED DATA ETAG ==================== private val _seededDataETag = MutableStateFlow(null) val seededDataETag: StateFlow = _seededDataETag.asStateFlow() // ==================== INITIALIZATION ==================== /** * Initialize DataManager with platform-specific managers. * Call this once at app startup. */ fun initialize( tokenMgr: TokenManager, themeMgr: ThemeStorageManager, persistenceMgr: PersistenceManager ) { tokenManager = tokenMgr themeManager = themeMgr persistenceManager = persistenceMgr // Load auth token from secure storage _authToken.value = tokenMgr.getToken() // Load theme preference _themeId.value = themeMgr.getThemeId() ?: "default" // Load cached data from disk loadFromDisk() _isInitialized.value = true } /** * Check if user is authenticated (has valid token) */ fun isAuthenticated(): Boolean = _authToken.value != null // ==================== O(1) LOOKUP HELPERS ==================== fun getResidenceType(id: Int?): ResidenceType? = id?.let { _residenceTypesMap.value[it] } fun getTaskFrequency(id: Int?): TaskFrequency? = id?.let { _taskFrequenciesMap.value[it] } fun getTaskPriority(id: Int?): TaskPriority? = id?.let { _taskPrioritiesMap.value[it] } fun getTaskCategory(id: Int?): TaskCategory? = id?.let { _taskCategoriesMap.value[it] } fun getContractorSpecialty(id: Int?): ContractorSpecialty? = id?.let { _contractorSpecialtiesMap.value[it] } // ==================== AUTH UPDATE METHODS ==================== fun setAuthToken(token: String?) { _authToken.value = token if (token != null) { tokenManager?.saveToken(token) } else { tokenManager?.clearToken() } } fun setCurrentUser(user: User?) { _currentUser.value = user persistToDisk() } // ==================== THEME UPDATE METHODS ==================== fun setThemeId(id: String) { _themeId.value = id themeManager?.saveThemeId(id) } // ==================== ONBOARDING UPDATE METHODS ==================== fun setHasCompletedOnboarding(completed: Boolean) { _hasCompletedOnboarding.value = completed persistenceManager?.save(KEY_HAS_COMPLETED_ONBOARDING, completed.toString()) } // ==================== RESIDENCE UPDATE METHODS ==================== fun setResidences(residences: List) { _residences.value = residences residencesCacheTime = currentTimeMs() updateLastSyncTime() persistToDisk() } fun setMyResidences(response: MyResidencesResponse) { _myResidences.value = response // Also update totalSummary from myResidences response _totalSummary.value = response.summary myResidencesCacheTime = currentTimeMs() summaryCacheTime = currentTimeMs() updateLastSyncTime() persistToDisk() } fun setTotalSummary(summary: TotalSummary) { _totalSummary.value = summary // Also update the summary in myResidences if it exists _myResidences.value?.let { current -> _myResidences.value = current.copy(summary = summary) } summaryCacheTime = currentTimeMs() persistToDisk() } /** * Calculate TotalSummary from cached kanban data. * This allows the client to compute summary stats without waiting for API responses * after CRUD operations (API now returns empty summaries for performance). * * Columns from API: overdue_tasks, in_progress_tasks, due_soon_tasks, upcoming_tasks, completed_tasks, cancelled_tasks */ fun calculateSummaryFromKanban(): TotalSummary { val kanban = _allTasks.value ?: return _totalSummary.value ?: TotalSummary() val residenceCount = _myResidences.value?.residences?.size ?: _residences.value.size var overdueCount = 0 var inProgressCount = 0 var dueSoonCount = 0 var upcomingCount = 0 var completedCount = 0 for (column in kanban.columns) { when (column.name) { "overdue_tasks" -> overdueCount = column.count "in_progress_tasks" -> inProgressCount = column.count "due_soon_tasks" -> dueSoonCount = column.count "upcoming_tasks" -> upcomingCount = column.count "completed_tasks" -> completedCount = column.count // cancelled_tasks is not counted in totals } } // totalTasks = all non-cancelled tasks val totalTasks = overdueCount + inProgressCount + dueSoonCount + upcomingCount + completedCount // totalPending = not completed (i.e., still needs attention) val totalPending = overdueCount + inProgressCount + dueSoonCount + upcomingCount return TotalSummary( totalResidences = residenceCount, totalTasks = totalTasks, totalPending = totalPending, totalOverdue = overdueCount, // due_soon_tasks column = tasks due within threshold (default 30 days) // upcoming_tasks column = tasks with dates beyond threshold OR no due date // Both "Due This Week" and "Next 30 Days" use due_soon since it represents the 30-day window tasksDueNextWeek = dueSoonCount, tasksDueNextMonth = dueSoonCount ) } /** * Update totalSummary from cached kanban data. * Call this after task CRUD operations to keep summary in sync without API call. */ fun refreshSummaryFromKanban() { val calculatedSummary = calculateSummaryFromKanban() _totalSummary.value = calculatedSummary // Also update myResidences summary if present _myResidences.value?.let { current -> _myResidences.value = current.copy(summary = calculatedSummary) } } fun setResidenceSummary(residenceId: Int, summary: ResidenceSummaryResponse) { _residenceSummaries.value = _residenceSummaries.value + (residenceId to summary) persistToDisk() } fun addResidence(residence: Residence) { _residences.value = _residences.value + residence updateLastSyncTime() persistToDisk() } fun updateResidence(residence: Residence) { _residences.value = _residences.value.map { if (it.id == residence.id) residence else it } persistToDisk() } fun removeResidence(residenceId: Int) { _residences.value = _residences.value.filter { it.id != residenceId } _tasksByResidence.value = _tasksByResidence.value - residenceId _documentsByResidence.value = _documentsByResidence.value - residenceId _residenceSummaries.value = _residenceSummaries.value - residenceId // Also update myResidences so the list view updates immediately _myResidences.value?.let { current -> _myResidences.value = current.copy( residences = current.residences.filter { it.id != residenceId } ) } persistToDisk() } // ==================== TASK UPDATE METHODS ==================== fun setAllTasks(response: TaskColumnsResponse) { _allTasks.value = response tasksCacheTime = currentTimeMs() updateLastSyncTime() // Refresh summary from kanban data (API no longer returns summary stats) refreshSummaryFromKanban() persistToDisk() } fun setTasksForResidence(residenceId: Int, response: TaskColumnsResponse) { _tasksByResidence.value = _tasksByResidence.value + (residenceId to response) tasksByResidenceCacheTime[residenceId] = currentTimeMs() persistToDisk() } /** * Filter cached allTasks by residence ID to avoid separate API call. * Returns null if allTasks not cached. * This enables client-side filtering when we already have all tasks loaded. */ fun getTasksForResidence(residenceId: Int): TaskColumnsResponse? { val allTasksData = _allTasks.value ?: return null // Filter each column's tasks by residence ID val filteredColumns = allTasksData.columns.map { column -> column.copy( tasks = column.tasks.filter { it.residenceId == residenceId }, count = column.tasks.count { it.residenceId == residenceId } ) } return TaskColumnsResponse( columns = filteredColumns, daysThreshold = allTasksData.daysThreshold, residenceId = residenceId.toString(), summary = null // Summary is global; residence-specific not available client-side ) } /** * Update a single task - moves it to the correct kanban column based on kanban_column field. * This is called after task completion, status change, etc. * Also refreshes the summary from the updated kanban data. */ fun updateTask(task: TaskResponse) { // Update in allTasks _allTasks.value?.let { current -> val targetColumn = task.kanbanColumn ?: "upcoming_tasks" val newColumns = current.columns.map { column -> // Remove task from this column if present val filteredTasks = column.tasks.filter { it.id != task.id } // Add task if this is the target column val updatedTasks = if (column.name == targetColumn) { filteredTasks + task } else { filteredTasks } column.copy(tasks = updatedTasks, count = updatedTasks.size) } _allTasks.value = current.copy(columns = newColumns) } // Update in tasksByResidence if this task's residence is cached task.residenceId?.let { residenceId -> _tasksByResidence.value[residenceId]?.let { current -> val targetColumn = task.kanbanColumn ?: "upcoming_tasks" val newColumns = current.columns.map { column -> val filteredTasks = column.tasks.filter { it.id != task.id } val updatedTasks = if (column.name == targetColumn) { filteredTasks + task } else { filteredTasks } column.copy(tasks = updatedTasks, count = updatedTasks.size) } _tasksByResidence.value = _tasksByResidence.value + (residenceId to current.copy(columns = newColumns)) } } // Refresh summary from updated kanban data (API no longer returns summaries for CRUD) refreshSummaryFromKanban() persistToDisk() } fun removeTask(taskId: Int) { // Remove from allTasks _allTasks.value?.let { current -> val newColumns = current.columns.map { column -> val filteredTasks = column.tasks.filter { it.id != taskId } column.copy(tasks = filteredTasks, count = filteredTasks.size) } _allTasks.value = current.copy(columns = newColumns) } // Remove from all residence task caches _tasksByResidence.value = _tasksByResidence.value.mapValues { (_, tasks) -> val newColumns = tasks.columns.map { column -> val filteredTasks = column.tasks.filter { it.id != taskId } column.copy(tasks = filteredTasks, count = filteredTasks.size) } tasks.copy(columns = newColumns) } // Refresh summary from updated kanban data (API no longer returns summaries for CRUD) refreshSummaryFromKanban() persistToDisk() } // ==================== DOCUMENT UPDATE METHODS ==================== fun setDocuments(documents: List) { _documents.value = documents documentsCacheTime = currentTimeMs() updateLastSyncTime() persistToDisk() } fun setDocumentsForResidence(residenceId: Int, documents: List) { _documentsByResidence.value = _documentsByResidence.value + (residenceId to documents) persistToDisk() } fun addDocument(document: Document) { _documents.value = _documents.value + document persistToDisk() } fun updateDocument(document: Document) { _documents.value = _documents.value.map { if (it.id == document.id) document else it } persistToDisk() } fun removeDocument(documentId: Int) { _documents.value = _documents.value.filter { it.id != documentId } // Also remove from residence-specific caches _documentsByResidence.value = _documentsByResidence.value.mapValues { (_, docs) -> docs.filter { it.id != documentId } } persistToDisk() } // ==================== CONTRACTOR UPDATE METHODS ==================== fun setContractors(contractors: List) { _contractors.value = contractors contractorsCacheTime = currentTimeMs() updateLastSyncTime() persistToDisk() } fun addContractor(contractor: ContractorSummary) { _contractors.value = _contractors.value + contractor persistToDisk() } /** Add a full Contractor (converts to summary for storage) */ fun addContractor(contractor: Contractor) { _contractors.value = _contractors.value + contractor.toSummary() persistToDisk() } fun updateContractor(contractor: ContractorSummary) { _contractors.value = _contractors.value.map { if (it.id == contractor.id) contractor else it } persistToDisk() } /** Update from a full Contractor (converts to summary for storage) */ fun updateContractor(contractor: Contractor) { val summary = contractor.toSummary() _contractors.value = _contractors.value.map { if (it.id == summary.id) summary else it } persistToDisk() } fun removeContractor(contractorId: Int) { _contractors.value = _contractors.value.filter { it.id != contractorId } persistToDisk() } // ==================== SUBSCRIPTION UPDATE METHODS ==================== fun setSubscription(subscription: SubscriptionStatus) { _subscription.value = subscription persistToDisk() } fun setUpgradeTriggers(triggers: Map) { _upgradeTriggers.value = triggers persistToDisk() } fun setFeatureBenefits(benefits: List) { _featureBenefits.value = benefits persistToDisk() } fun setPromotions(promos: List) { _promotions.value = promos persistToDisk() } // ==================== LOOKUP UPDATE METHODS ==================== fun setResidenceTypes(types: List) { _residenceTypes.value = types _residenceTypesMap.value = types.associateBy { it.id } persistToDisk() } fun setTaskFrequencies(frequencies: List) { _taskFrequencies.value = frequencies _taskFrequenciesMap.value = frequencies.associateBy { it.id } persistToDisk() } fun setTaskPriorities(priorities: List) { _taskPriorities.value = priorities _taskPrioritiesMap.value = priorities.associateBy { it.id } persistToDisk() } fun setTaskCategories(categories: List) { _taskCategories.value = categories _taskCategoriesMap.value = categories.associateBy { it.id } persistToDisk() } fun setContractorSpecialties(specialties: List) { _contractorSpecialties.value = specialties _contractorSpecialtiesMap.value = specialties.associateBy { it.id } persistToDisk() } // ==================== TASK TEMPLATE UPDATE METHODS ==================== fun setTaskTemplates(templates: List) { _taskTemplates.value = templates // Don't persist - these are fetched fresh from API } fun setTaskTemplatesGrouped(response: TaskTemplatesGroupedResponse) { _taskTemplatesGrouped.value = response // Also extract flat list from grouped response val flatList = response.categories.flatMap { it.templates } _taskTemplates.value = flatList // Don't persist - these are fetched fresh from API } /** * Search task templates by query string (local search) */ fun searchTaskTemplates(query: String): List { if (query.length < 2) return emptyList() val lowercaseQuery = query.lowercase() return _taskTemplates.value.filter { template -> template.title.lowercase().contains(lowercaseQuery) || template.description.lowercase().contains(lowercaseQuery) || template.tags.any { it.lowercase().contains(lowercaseQuery) } }.take(10) } fun setAllLookups(staticData: StaticDataResponse) { setResidenceTypes(staticData.residenceTypes) setTaskFrequencies(staticData.taskFrequencies) setTaskPriorities(staticData.taskPriorities) setTaskCategories(staticData.taskCategories) setContractorSpecialties(staticData.contractorSpecialties) _lookupsInitialized.value = true } /** * Set all lookups from unified seeded data response. * Also stores the ETag for future conditional requests. * Persists lookup data to disk for faster app startup. */ fun setAllLookupsFromSeededData(seededData: SeededDataResponse, etag: String?) { setResidenceTypes(seededData.residenceTypes) setTaskFrequencies(seededData.taskFrequencies) setTaskPriorities(seededData.taskPriorities) setTaskCategories(seededData.taskCategories) setContractorSpecialties(seededData.contractorSpecialties) setTaskTemplatesGrouped(seededData.taskTemplates) setSeededDataETag(etag) _lookupsInitialized.value = true // Persist lookups to disk for faster startup persistLookupsToDisk() } /** * Set the ETag for seeded data. Used for conditional requests. */ fun setSeededDataETag(etag: String?) { _seededDataETag.value = etag if (etag != null) { persistenceManager?.save(KEY_SEEDED_DATA_ETAG, etag) } else { persistenceManager?.remove(KEY_SEEDED_DATA_ETAG) } } fun markLookupsInitialized() { _lookupsInitialized.value = true } // ==================== CLEAR METHODS ==================== /** * Clear all data - called on logout */ fun clear() { // Clear auth _authToken.value = null _currentUser.value = null tokenManager?.clearToken() // Clear user data _residences.value = emptyList() _myResidences.value = null _totalSummary.value = null _residenceSummaries.value = emptyMap() _allTasks.value = null _tasksByResidence.value = emptyMap() _documents.value = emptyList() _documentsByResidence.value = emptyMap() _contractors.value = emptyList() // Clear subscription _subscription.value = null _upgradeTriggers.value = emptyMap() _featureBenefits.value = emptyList() _promotions.value = emptyList() // Clear lookups _residenceTypes.value = emptyList() _residenceTypesMap.value = emptyMap() _taskFrequencies.value = emptyList() _taskFrequenciesMap.value = emptyMap() _taskPriorities.value = emptyList() _taskPrioritiesMap.value = emptyMap() _taskCategories.value = emptyList() _taskCategoriesMap.value = emptyMap() _contractorSpecialties.value = emptyList() _contractorSpecialtiesMap.value = emptyMap() _taskTemplates.value = emptyList() _taskTemplatesGrouped.value = null _lookupsInitialized.value = false _seededDataETag.value = null // Clear cache timestamps residencesCacheTime = 0L myResidencesCacheTime = 0L tasksCacheTime = 0L tasksByResidenceCacheTime.clear() contractorsCacheTime = 0L documentsCacheTime = 0L summaryCacheTime = 0L // Clear metadata _lastSyncTime.value = 0L // Clear persistent storage (except theme) persistenceManager?.clear() } /** * Clear only user-specific data (keep lookups and preferences) */ fun clearUserData() { _currentUser.value = null _residences.value = emptyList() _myResidences.value = null _totalSummary.value = null _residenceSummaries.value = emptyMap() _allTasks.value = null _tasksByResidence.value = emptyMap() _documents.value = emptyList() _documentsByResidence.value = emptyMap() _contractors.value = emptyList() _subscription.value = null _upgradeTriggers.value = emptyMap() _featureBenefits.value = emptyList() _promotions.value = emptyList() // Clear cache timestamps residencesCacheTime = 0L myResidencesCacheTime = 0L tasksCacheTime = 0L tasksByResidenceCacheTime.clear() contractorsCacheTime = 0L documentsCacheTime = 0L summaryCacheTime = 0L persistToDisk() } // ==================== PERSISTENCE ==================== @OptIn(ExperimentalTime::class) private fun updateLastSyncTime() { _lastSyncTime.value = Clock.System.now().toEpochMilliseconds() } /** * Persist current state to disk. * Persists user data and lookup data for faster app startup. */ private fun persistToDisk() { val manager = persistenceManager ?: return try { _currentUser.value?.let { manager.save(KEY_CURRENT_USER, json.encodeToString(it)) } } catch (e: Exception) { println("DataManager: Error persisting user to disk: ${e.message}") } } /** * Persist lookup data to disk separately (called less frequently). */ private fun persistLookupsToDisk() { val manager = persistenceManager ?: return try { if (_residenceTypes.value.isNotEmpty()) { manager.save(KEY_RESIDENCE_TYPES, json.encodeToString(_residenceTypes.value)) } if (_taskFrequencies.value.isNotEmpty()) { manager.save(KEY_TASK_FREQUENCIES, json.encodeToString(_taskFrequencies.value)) } if (_taskPriorities.value.isNotEmpty()) { manager.save(KEY_TASK_PRIORITIES, json.encodeToString(_taskPriorities.value)) } if (_taskCategories.value.isNotEmpty()) { manager.save(KEY_TASK_CATEGORIES, json.encodeToString(_taskCategories.value)) } if (_contractorSpecialties.value.isNotEmpty()) { manager.save(KEY_CONTRACTOR_SPECIALTIES, json.encodeToString(_contractorSpecialties.value)) } _taskTemplatesGrouped.value?.let { manager.save(KEY_TASK_TEMPLATES_GROUPED, json.encodeToString(it)) } println("DataManager: Lookup data persisted to disk") } catch (e: Exception) { println("DataManager: Error persisting lookups to disk: ${e.message}") } } /** * Load cached state from disk. * Loads user data and lookup data for faster app startup. */ private fun loadFromDisk() { val manager = persistenceManager ?: return try { // Load user data manager.load(KEY_CURRENT_USER)?.let { data -> _currentUser.value = json.decodeFromString(data) } // Load onboarding completion flag manager.load(KEY_HAS_COMPLETED_ONBOARDING)?.let { data -> _hasCompletedOnboarding.value = data.toBooleanStrictOrNull() ?: false } // Load seeded data ETag for conditional requests manager.load(KEY_SEEDED_DATA_ETAG)?.let { data -> _seededDataETag.value = data } // Load lookup data loadLookupsFromDisk() } catch (e: Exception) { println("DataManager: Error loading from disk: ${e.message}") } } /** * Load lookup data from disk. * If lookups are successfully loaded, mark as initialized. */ private fun loadLookupsFromDisk() { val manager = persistenceManager ?: return var lookupsLoaded = false try { manager.load(KEY_RESIDENCE_TYPES)?.let { data -> val types = json.decodeFromString>(data) _residenceTypes.value = types _residenceTypesMap.value = types.associateBy { it.id } lookupsLoaded = true } manager.load(KEY_TASK_FREQUENCIES)?.let { data -> val frequencies = json.decodeFromString>(data) _taskFrequencies.value = frequencies _taskFrequenciesMap.value = frequencies.associateBy { it.id } } manager.load(KEY_TASK_PRIORITIES)?.let { data -> val priorities = json.decodeFromString>(data) _taskPriorities.value = priorities _taskPrioritiesMap.value = priorities.associateBy { it.id } } manager.load(KEY_TASK_CATEGORIES)?.let { data -> val categories = json.decodeFromString>(data) _taskCategories.value = categories _taskCategoriesMap.value = categories.associateBy { it.id } } manager.load(KEY_CONTRACTOR_SPECIALTIES)?.let { data -> val specialties = json.decodeFromString>(data) _contractorSpecialties.value = specialties _contractorSpecialtiesMap.value = specialties.associateBy { it.id } } manager.load(KEY_TASK_TEMPLATES_GROUPED)?.let { data -> val grouped = json.decodeFromString(data) _taskTemplatesGrouped.value = grouped _taskTemplates.value = grouped.categories.flatMap { it.templates } } // Mark lookups as initialized if we loaded any data if (lookupsLoaded) { _lookupsInitialized.value = true println("DataManager: Lookup data loaded from disk") } } catch (e: Exception) { println("DataManager: Error loading lookups from disk: ${e.message}") } } // ==================== PERSISTENCE KEYS ==================== private const val KEY_CURRENT_USER = "dm_current_user" private const val KEY_HAS_COMPLETED_ONBOARDING = "dm_has_completed_onboarding" private const val KEY_SEEDED_DATA_ETAG = "dm_seeded_data_etag" private const val KEY_RESIDENCE_TYPES = "dm_residence_types" private const val KEY_TASK_FREQUENCIES = "dm_task_frequencies" private const val KEY_TASK_PRIORITIES = "dm_task_priorities" private const val KEY_TASK_CATEGORIES = "dm_task_categories" private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties" private const val KEY_TASK_TEMPLATES_GROUPED = "dm_task_templates_grouped" }