Add unified DataManager as single source of truth for all app data
- Create DataManager.kt with StateFlows for all cached data: - Authentication (token, user) - Residences, tasks, documents, contractors - Subscription status and upgrade triggers - All lookup data (residence types, task categories, etc.) - Theme preferences and state metadata - Add PersistenceManager with platform-specific implementations: - Android: SharedPreferences - iOS: NSUserDefaults - JVM: Properties file - WasmJS: localStorage - Migrate APILayer to update DataManager on every API response - Update Kotlin ViewModels to use DataManager for token access - Deprecate LookupsRepository (delegates to DataManager) - Create iOS DataManagerObservable Swift wrapper for SwiftUI - Update iOS auth flow to use DataManager.isAuthenticated() Data flow: User Action → API Call → DataManager Updated → All Screens React 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
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 {
|
||||
|
||||
// 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<String?>(null)
|
||||
val authToken: StateFlow<String?> = _authToken.asStateFlow()
|
||||
|
||||
private val _currentUser = MutableStateFlow<User?>(null)
|
||||
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
|
||||
|
||||
// ==================== APP PREFERENCES ====================
|
||||
|
||||
private val _themeId = MutableStateFlow("default")
|
||||
val themeId: StateFlow<String> = _themeId.asStateFlow()
|
||||
|
||||
// ==================== RESIDENCES ====================
|
||||
|
||||
private val _residences = MutableStateFlow<List<Residence>>(emptyList())
|
||||
val residences: StateFlow<List<Residence>> = _residences.asStateFlow()
|
||||
|
||||
private val _myResidences = MutableStateFlow<MyResidencesResponse?>(null)
|
||||
val myResidences: StateFlow<MyResidencesResponse?> = _myResidences.asStateFlow()
|
||||
|
||||
private val _residenceSummaries = MutableStateFlow<Map<Int, ResidenceSummaryResponse>>(emptyMap())
|
||||
val residenceSummaries: StateFlow<Map<Int, ResidenceSummaryResponse>> = _residenceSummaries.asStateFlow()
|
||||
|
||||
// ==================== TASKS ====================
|
||||
|
||||
private val _allTasks = MutableStateFlow<TaskColumnsResponse?>(null)
|
||||
val allTasks: StateFlow<TaskColumnsResponse?> = _allTasks.asStateFlow()
|
||||
|
||||
private val _tasksByResidence = MutableStateFlow<Map<Int, TaskColumnsResponse>>(emptyMap())
|
||||
val tasksByResidence: StateFlow<Map<Int, TaskColumnsResponse>> = _tasksByResidence.asStateFlow()
|
||||
|
||||
// ==================== DOCUMENTS ====================
|
||||
|
||||
private val _documents = MutableStateFlow<List<Document>>(emptyList())
|
||||
val documents: StateFlow<List<Document>> = _documents.asStateFlow()
|
||||
|
||||
private val _documentsByResidence = MutableStateFlow<Map<Int, List<Document>>>(emptyMap())
|
||||
val documentsByResidence: StateFlow<Map<Int, List<Document>>> = _documentsByResidence.asStateFlow()
|
||||
|
||||
// ==================== CONTRACTORS ====================
|
||||
|
||||
private val _contractors = MutableStateFlow<List<Contractor>>(emptyList())
|
||||
val contractors: StateFlow<List<Contractor>> = _contractors.asStateFlow()
|
||||
|
||||
// ==================== SUBSCRIPTION ====================
|
||||
|
||||
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
||||
val subscription: StateFlow<SubscriptionStatus?> = _subscription.asStateFlow()
|
||||
|
||||
private val _upgradeTriggers = MutableStateFlow<Map<String, UpgradeTriggerData>>(emptyMap())
|
||||
val upgradeTriggers: StateFlow<Map<String, UpgradeTriggerData>> = _upgradeTriggers.asStateFlow()
|
||||
|
||||
private val _featureBenefits = MutableStateFlow<List<FeatureBenefit>>(emptyList())
|
||||
val featureBenefits: StateFlow<List<FeatureBenefit>> = _featureBenefits.asStateFlow()
|
||||
|
||||
private val _promotions = MutableStateFlow<List<Promotion>>(emptyList())
|
||||
val promotions: StateFlow<List<Promotion>> = _promotions.asStateFlow()
|
||||
|
||||
// ==================== LOOKUPS (Reference Data) ====================
|
||||
|
||||
// List-based for dropdowns/pickers
|
||||
private val _residenceTypes = MutableStateFlow<List<ResidenceType>>(emptyList())
|
||||
val residenceTypes: StateFlow<List<ResidenceType>> = _residenceTypes.asStateFlow()
|
||||
|
||||
private val _taskFrequencies = MutableStateFlow<List<TaskFrequency>>(emptyList())
|
||||
val taskFrequencies: StateFlow<List<TaskFrequency>> = _taskFrequencies.asStateFlow()
|
||||
|
||||
private val _taskPriorities = MutableStateFlow<List<TaskPriority>>(emptyList())
|
||||
val taskPriorities: StateFlow<List<TaskPriority>> = _taskPriorities.asStateFlow()
|
||||
|
||||
private val _taskStatuses = MutableStateFlow<List<TaskStatus>>(emptyList())
|
||||
val taskStatuses: StateFlow<List<TaskStatus>> = _taskStatuses.asStateFlow()
|
||||
|
||||
private val _taskCategories = MutableStateFlow<List<TaskCategory>>(emptyList())
|
||||
val taskCategories: StateFlow<List<TaskCategory>> = _taskCategories.asStateFlow()
|
||||
|
||||
private val _contractorSpecialties = MutableStateFlow<List<ContractorSpecialty>>(emptyList())
|
||||
val contractorSpecialties: StateFlow<List<ContractorSpecialty>> = _contractorSpecialties.asStateFlow()
|
||||
|
||||
// Map-based for O(1) ID resolution
|
||||
private val _residenceTypesMap = MutableStateFlow<Map<Int, ResidenceType>>(emptyMap())
|
||||
val residenceTypesMap: StateFlow<Map<Int, ResidenceType>> = _residenceTypesMap.asStateFlow()
|
||||
|
||||
private val _taskFrequenciesMap = MutableStateFlow<Map<Int, TaskFrequency>>(emptyMap())
|
||||
val taskFrequenciesMap: StateFlow<Map<Int, TaskFrequency>> = _taskFrequenciesMap.asStateFlow()
|
||||
|
||||
private val _taskPrioritiesMap = MutableStateFlow<Map<Int, TaskPriority>>(emptyMap())
|
||||
val taskPrioritiesMap: StateFlow<Map<Int, TaskPriority>> = _taskPrioritiesMap.asStateFlow()
|
||||
|
||||
private val _taskStatusesMap = MutableStateFlow<Map<Int, TaskStatus>>(emptyMap())
|
||||
val taskStatusesMap: StateFlow<Map<Int, TaskStatus>> = _taskStatusesMap.asStateFlow()
|
||||
|
||||
private val _taskCategoriesMap = MutableStateFlow<Map<Int, TaskCategory>>(emptyMap())
|
||||
val taskCategoriesMap: StateFlow<Map<Int, TaskCategory>> = _taskCategoriesMap.asStateFlow()
|
||||
|
||||
private val _contractorSpecialtiesMap = MutableStateFlow<Map<Int, ContractorSpecialty>>(emptyMap())
|
||||
val contractorSpecialtiesMap: StateFlow<Map<Int, ContractorSpecialty>> = _contractorSpecialtiesMap.asStateFlow()
|
||||
|
||||
// ==================== STATE METADATA ====================
|
||||
|
||||
private val _isInitialized = MutableStateFlow(false)
|
||||
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||
|
||||
private val _lookupsInitialized = MutableStateFlow(false)
|
||||
val lookupsInitialized: StateFlow<Boolean> = _lookupsInitialized.asStateFlow()
|
||||
|
||||
private val _lastSyncTime = MutableStateFlow(0L)
|
||||
val lastSyncTime: StateFlow<Long> = _lastSyncTime.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 getTaskStatus(id: Int?): TaskStatus? = id?.let { _taskStatusesMap.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)
|
||||
}
|
||||
|
||||
// ==================== RESIDENCE UPDATE METHODS ====================
|
||||
|
||||
fun setResidences(residences: List<Residence>) {
|
||||
_residences.value = residences
|
||||
updateLastSyncTime()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setMyResidences(response: MyResidencesResponse) {
|
||||
_myResidences.value = response
|
||||
updateLastSyncTime()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
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
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
// ==================== TASK UPDATE METHODS ====================
|
||||
|
||||
fun setAllTasks(response: TaskColumnsResponse) {
|
||||
_allTasks.value = response
|
||||
updateLastSyncTime()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTasksForResidence(residenceId: Int, response: TaskColumnsResponse) {
|
||||
_tasksByResidence.value = _tasksByResidence.value + (residenceId to response)
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
// ==================== DOCUMENT UPDATE METHODS ====================
|
||||
|
||||
fun setDocuments(documents: List<Document>) {
|
||||
_documents.value = documents
|
||||
updateLastSyncTime()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setDocumentsForResidence(residenceId: Int, documents: List<Document>) {
|
||||
_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<Contractor>) {
|
||||
_contractors.value = contractors
|
||||
updateLastSyncTime()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun addContractor(contractor: Contractor) {
|
||||
_contractors.value = _contractors.value + contractor
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun updateContractor(contractor: Contractor) {
|
||||
_contractors.value = _contractors.value.map {
|
||||
if (it.id == contractor.id) contractor 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<String, UpgradeTriggerData>) {
|
||||
_upgradeTriggers.value = triggers
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setFeatureBenefits(benefits: List<FeatureBenefit>) {
|
||||
_featureBenefits.value = benefits
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setPromotions(promos: List<Promotion>) {
|
||||
_promotions.value = promos
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
// ==================== LOOKUP UPDATE METHODS ====================
|
||||
|
||||
fun setResidenceTypes(types: List<ResidenceType>) {
|
||||
_residenceTypes.value = types
|
||||
_residenceTypesMap.value = types.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTaskFrequencies(frequencies: List<TaskFrequency>) {
|
||||
_taskFrequencies.value = frequencies
|
||||
_taskFrequenciesMap.value = frequencies.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTaskPriorities(priorities: List<TaskPriority>) {
|
||||
_taskPriorities.value = priorities
|
||||
_taskPrioritiesMap.value = priorities.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTaskStatuses(statuses: List<TaskStatus>) {
|
||||
_taskStatuses.value = statuses
|
||||
_taskStatusesMap.value = statuses.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setTaskCategories(categories: List<TaskCategory>) {
|
||||
_taskCategories.value = categories
|
||||
_taskCategoriesMap.value = categories.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setContractorSpecialties(specialties: List<ContractorSpecialty>) {
|
||||
_contractorSpecialties.value = specialties
|
||||
_contractorSpecialtiesMap.value = specialties.associateBy { it.id }
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
fun setAllLookups(staticData: StaticDataResponse) {
|
||||
setResidenceTypes(staticData.residenceTypes)
|
||||
setTaskFrequencies(staticData.taskFrequencies)
|
||||
setTaskPriorities(staticData.taskPriorities)
|
||||
setTaskStatuses(staticData.taskStatuses)
|
||||
setTaskCategories(staticData.taskCategories)
|
||||
setContractorSpecialties(staticData.contractorSpecialties)
|
||||
_lookupsInitialized.value = true
|
||||
}
|
||||
|
||||
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
|
||||
_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()
|
||||
_taskStatuses.value = emptyList()
|
||||
_taskStatusesMap.value = emptyMap()
|
||||
_taskCategories.value = emptyList()
|
||||
_taskCategoriesMap.value = emptyMap()
|
||||
_contractorSpecialties.value = emptyList()
|
||||
_contractorSpecialtiesMap.value = emptyMap()
|
||||
_lookupsInitialized.value = false
|
||||
|
||||
// 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
|
||||
_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()
|
||||
persistToDisk()
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE ====================
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun updateLastSyncTime() {
|
||||
_lastSyncTime.value = Clock.System.now().toEpochMilliseconds()
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist current state to disk.
|
||||
* Called automatically after each update.
|
||||
*/
|
||||
private fun persistToDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
|
||||
try {
|
||||
// Persist each data type
|
||||
_currentUser.value?.let {
|
||||
manager.save(KEY_CURRENT_USER, json.encodeToString(it))
|
||||
}
|
||||
|
||||
if (_residences.value.isNotEmpty()) {
|
||||
manager.save(KEY_RESIDENCES, json.encodeToString(_residences.value))
|
||||
}
|
||||
|
||||
_myResidences.value?.let {
|
||||
manager.save(KEY_MY_RESIDENCES, json.encodeToString(it))
|
||||
}
|
||||
|
||||
_allTasks.value?.let {
|
||||
manager.save(KEY_ALL_TASKS, json.encodeToString(it))
|
||||
}
|
||||
|
||||
if (_documents.value.isNotEmpty()) {
|
||||
manager.save(KEY_DOCUMENTS, json.encodeToString(_documents.value))
|
||||
}
|
||||
|
||||
if (_contractors.value.isNotEmpty()) {
|
||||
manager.save(KEY_CONTRACTORS, json.encodeToString(_contractors.value))
|
||||
}
|
||||
|
||||
_subscription.value?.let {
|
||||
manager.save(KEY_SUBSCRIPTION, json.encodeToString(it))
|
||||
}
|
||||
|
||||
// Persist lookups
|
||||
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 (_taskStatuses.value.isNotEmpty()) {
|
||||
manager.save(KEY_TASK_STATUSES, json.encodeToString(_taskStatuses.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))
|
||||
}
|
||||
|
||||
manager.save(KEY_LAST_SYNC_TIME, _lastSyncTime.value.toString())
|
||||
} catch (e: Exception) {
|
||||
// Log error but don't crash - persistence is best-effort
|
||||
println("DataManager: Error persisting to disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cached state from disk.
|
||||
* Called during initialization.
|
||||
*/
|
||||
private fun loadFromDisk() {
|
||||
val manager = persistenceManager ?: return
|
||||
|
||||
try {
|
||||
manager.load(KEY_CURRENT_USER)?.let { data ->
|
||||
_currentUser.value = json.decodeFromString<User>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_RESIDENCES)?.let { data ->
|
||||
_residences.value = json.decodeFromString<List<Residence>>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_MY_RESIDENCES)?.let { data ->
|
||||
_myResidences.value = json.decodeFromString<MyResidencesResponse>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_ALL_TASKS)?.let { data ->
|
||||
_allTasks.value = json.decodeFromString<TaskColumnsResponse>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_DOCUMENTS)?.let { data ->
|
||||
_documents.value = json.decodeFromString<List<Document>>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_CONTRACTORS)?.let { data ->
|
||||
_contractors.value = json.decodeFromString<List<Contractor>>(data)
|
||||
}
|
||||
|
||||
manager.load(KEY_SUBSCRIPTION)?.let { data ->
|
||||
_subscription.value = json.decodeFromString<SubscriptionStatus>(data)
|
||||
}
|
||||
|
||||
// Load lookups
|
||||
manager.load(KEY_RESIDENCE_TYPES)?.let { data ->
|
||||
val types = json.decodeFromString<List<ResidenceType>>(data)
|
||||
_residenceTypes.value = types
|
||||
_residenceTypesMap.value = types.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_FREQUENCIES)?.let { data ->
|
||||
val items = json.decodeFromString<List<TaskFrequency>>(data)
|
||||
_taskFrequencies.value = items
|
||||
_taskFrequenciesMap.value = items.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_PRIORITIES)?.let { data ->
|
||||
val items = json.decodeFromString<List<TaskPriority>>(data)
|
||||
_taskPriorities.value = items
|
||||
_taskPrioritiesMap.value = items.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_STATUSES)?.let { data ->
|
||||
val items = json.decodeFromString<List<TaskStatus>>(data)
|
||||
_taskStatuses.value = items
|
||||
_taskStatusesMap.value = items.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_TASK_CATEGORIES)?.let { data ->
|
||||
val items = json.decodeFromString<List<TaskCategory>>(data)
|
||||
_taskCategories.value = items
|
||||
_taskCategoriesMap.value = items.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_CONTRACTOR_SPECIALTIES)?.let { data ->
|
||||
val items = json.decodeFromString<List<ContractorSpecialty>>(data)
|
||||
_contractorSpecialties.value = items
|
||||
_contractorSpecialtiesMap.value = items.associateBy { it.id }
|
||||
}
|
||||
|
||||
manager.load(KEY_LAST_SYNC_TIME)?.let { data ->
|
||||
_lastSyncTime.value = data.toLongOrNull() ?: 0L
|
||||
}
|
||||
|
||||
// Mark lookups initialized if we have data
|
||||
if (_residenceTypes.value.isNotEmpty()) {
|
||||
_lookupsInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log error but don't crash - cache miss is OK
|
||||
println("DataManager: Error loading from disk: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE KEYS ====================
|
||||
|
||||
private const val KEY_CURRENT_USER = "dm_current_user"
|
||||
private const val KEY_RESIDENCES = "dm_residences"
|
||||
private const val KEY_MY_RESIDENCES = "dm_my_residences"
|
||||
private const val KEY_ALL_TASKS = "dm_all_tasks"
|
||||
private const val KEY_DOCUMENTS = "dm_documents"
|
||||
private const val KEY_CONTRACTORS = "dm_contractors"
|
||||
private const val KEY_SUBSCRIPTION = "dm_subscription"
|
||||
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_STATUSES = "dm_task_statuses"
|
||||
private const val KEY_TASK_CATEGORIES = "dm_task_categories"
|
||||
private const val KEY_CONTRACTOR_SPECIALTIES = "dm_contractor_specialties"
|
||||
private const val KEY_LAST_SYNC_TIME = "dm_last_sync_time"
|
||||
}
|
||||
Reference in New Issue
Block a user