Add contractor sharing feature and move settings to navigation bar

Contractor Sharing:
- Add .casera file format for sharing contractors between users
- Create SharedContractor model with JSON serialization
- Implement ContractorSharingManager for iOS (Swift) and Android (Kotlin)
- Register .casera file type in iOS Info.plist and Android manifest
- Add share button to ContractorDetailView (iOS) and ContractorDetailScreen (Android)
- Add import confirmation, success, and error dialogs
- Create expect/actual platform implementations for sharing and import handling

Navigation Changes:
- Remove Profile tab from bottom tab bar (iOS and Android)
- Add settings gear icon to left side of "My Properties" title
- Settings gear opens Profile/Settings screen as sheet (iOS) or navigates (Android)
- Add property button to top right action bar

Bug Fixes:
- Fix ResidenceUsersResponse to match API's flat array response format
- Fix GenerateShareCodeResponse handling to access nested shareCode property
- Update ManageUsersDialog to accept residenceOwnerId parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-05 22:30:19 -06:00
parent 2965ec4031
commit 859a6679ed
43 changed files with 1848 additions and 148 deletions

View File

@@ -205,6 +205,11 @@ object DataManager {
private val _lastSyncTime = MutableStateFlow(0L)
val lastSyncTime: StateFlow<Long> = _lastSyncTime.asStateFlow()
// ==================== SEEDED DATA ETAG ====================
private val _seededDataETag = MutableStateFlow<String?>(null)
val seededDataETag: StateFlow<String?> = _seededDataETag.asStateFlow()
// ==================== INITIALIZATION ====================
/**
@@ -584,6 +589,34 @@ object DataManager {
_lookupsInitialized.value = true
}
/**
* Set all lookups from unified seeded data response.
* Also stores the ETag for future conditional requests.
*/
fun setAllLookupsFromSeededData(seededData: SeededDataResponse, etag: String?) {
setResidenceTypes(seededData.residenceTypes)
setTaskFrequencies(seededData.taskFrequencies)
setTaskPriorities(seededData.taskPriorities)
setTaskStatuses(seededData.taskStatuses)
setTaskCategories(seededData.taskCategories)
setContractorSpecialties(seededData.contractorSpecialties)
setTaskTemplatesGrouped(seededData.taskTemplates)
setSeededDataETag(etag)
_lookupsInitialized.value = true
}
/**
* 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
}
@@ -632,6 +665,7 @@ object DataManager {
_taskTemplates.value = emptyList()
_taskTemplatesGrouped.value = null
_lookupsInitialized.value = false
_seededDataETag.value = null
// Clear cache timestamps
residencesCacheTime = 0L
@@ -723,6 +757,11 @@ object DataManager {
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
}
} catch (e: Exception) {
println("DataManager: Error loading from disk: ${e.message}")
}
@@ -733,4 +772,5 @@ object DataManager {
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"
}