diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ef8ab951..17aa68374 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ plugins { android { defaultConfig { - val buildVersion = 256 + val buildVersion = 264 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" @@ -43,7 +43,6 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", "proguard-playservices.pro", - "proguard-retrofit2.pro", "proguard-crashlytics.pro", ) diff --git a/app/proguard-retrofit2.pro b/app/proguard-retrofit2.pro deleted file mode 100644 index 6c1daaf0b..000000000 --- a/app/proguard-retrofit2.pro +++ /dev/null @@ -1,48 +0,0 @@ -# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and -# EnclosingMethod is required to use InnerClasses. --keepattributes Signature, InnerClasses, EnclosingMethod - -# Retrofit does reflection on method and parameter annotations. --keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations - -# Keep annotation default values (e.g., retrofit2.http.Field.encoded). --keepattributes AnnotationDefault - -# Retain service method parameters when optimizing. --keepclassmembers,allowshrinking,allowobfuscation interface * { - @retrofit2.http.* ; -} - -# Ignore annotation used for build tooling. --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement - -# Ignore JSR 305 annotations for embedding nullability information. --dontwarn javax.annotation.** - -# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. --dontwarn kotlin.Unit - -# Top-level functions that can only be used by Kotlin. --dontwarn retrofit2.KotlinExtensions --dontwarn retrofit2.KotlinExtensions$* - -# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy -# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. --if interface * { @retrofit2.http.* ; } --keep,allowobfuscation interface <1> - -# Keep inherited services. --if interface * { @retrofit2.http.* ; } --keep,allowobfuscation interface * extends <1> - -# With R8 full mode generic signatures are stripped for classes that are not -# kept. Suspend functions are wrapped in continuations where the type argument -# is used. --keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation - -# R8 full mode strips generic signatures from return types if not kept. --if interface * { @retrofit2.http.* public *** *(...); } --keep,allowoptimization,allowshrinking,allowobfuscation class <3> - -# With R8 full mode generic signatures are stripped for classes that are not kept. --keep,allowobfuscation,allowshrinking class retrofit2.Response \ No newline at end of file diff --git a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt index a31437f60..b80f25ed9 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt @@ -57,7 +57,7 @@ import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( private val appPreferencesRepository: LocalAppPreferencesRepository, - private val appMetricsRepository: LocalAppMetricsRepository, + appMetricsRepository: LocalAppMetricsRepository, accountDataRepository: AccountDataRepository, incidentSelector: IncidentSelector, appDataRepository: AppDataManagementRepository, @@ -91,6 +91,8 @@ class MainActivityViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000), ) + val isAppUpdateAvailable = appMetricsRepository.isAppUpdateAvailable + /** * API account tokens need re-issuing */ diff --git a/app/src/main/java/com/crisiscleanup/ui/AppNavigation.kt b/app/src/main/java/com/crisiscleanup/ui/AppNavigation.kt index ec230d799..1d1111e82 100644 --- a/app/src/main/java/com/crisiscleanup/ui/AppNavigation.kt +++ b/app/src/main/java/com/crisiscleanup/ui/AppNavigation.kt @@ -1,7 +1,10 @@ package com.crisiscleanup.ui import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -13,6 +16,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -25,8 +29,10 @@ import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationBarI import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationDefaults import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRail import com.crisiscleanup.core.designsystem.component.CrisisCleanupNavigationRailItem +import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.icon.Icon import com.crisiscleanup.core.designsystem.theme.disabledAlpha +import com.crisiscleanup.core.designsystem.theme.primaryOrangeColor import com.crisiscleanup.navigation.TopLevelDestination @Composable @@ -61,6 +67,7 @@ private fun NavItems( destinations: List, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, + isAppUpdateAvailable: Boolean, itemContent: @Composable ( Boolean, String, @@ -77,12 +84,21 @@ private fun NavItems( t(destination.titleTranslateKey) } val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + val badgeImage = if (isAppUpdateAvailable && destination == TopLevelDestination.MENU) { + CrisisCleanupIcons.AppUpdateAvailable + } else { + null + } itemContent( selected, title, i == destinations.size - 1, { onNavigateToDestination(destination) }, - { destination.Icon(selected, title) }, + { + BadgedView(badgeImage) { + destination.Icon(selected, title) + } + }, { Text( title, @@ -98,6 +114,7 @@ internal fun AppNavigationBar( destinations: List, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, + isAppUpdateAvailable: Boolean, modifier: Modifier = Modifier, isRail: Boolean = false, ) { @@ -106,6 +123,7 @@ internal fun AppNavigationBar( destinations, onNavigateToDestination, currentDestination, + isAppUpdateAvailable, modifier, ) } else { @@ -113,6 +131,7 @@ internal fun AppNavigationBar( destinations, onNavigateToDestination, currentDestination, + isAppUpdateAvailable, modifier, ) } @@ -121,6 +140,7 @@ internal fun AppNavigationBar( @Composable internal fun AppNavigationBar( appState: CrisisCleanupAppState, + isAppUpdateAvailable: Boolean, modifier: Modifier = Modifier, isRail: Boolean = false, ) { @@ -128,16 +148,44 @@ internal fun AppNavigationBar( appState.topLevelDestinations, appState::navigateToTopLevelDestination, appState.currentDestination, + isAppUpdateAvailable = isAppUpdateAvailable, modifier, isRail, ) } +@Composable +private fun BadgedView( + badgeIcon: ImageVector?, + content: @Composable () -> Unit, +) { + if (badgeIcon == null) { + content() + } else { + BadgedBox( + badge = { + Badge( + Modifier.size(20.dp), + containerColor = primaryOrangeColor, + ) { + Icon( + imageVector = badgeIcon, + contentDescription = null, + ) + } + }, + ) { + content() + } + } +} + @Composable private fun CrisisCleanupNavRail( destinations: List, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, + isAppUpdateAvailable: Boolean, modifier: Modifier = Modifier, ) { CrisisCleanupNavigationRail(modifier = modifier) { @@ -146,6 +194,7 @@ private fun CrisisCleanupNavRail( destinations = destinations, onNavigateToDestination = onNavigateToDestination, currentDestination = currentDestination, + isAppUpdateAvailable = isAppUpdateAvailable, ) { isSelected: Boolean, title: String, @@ -180,6 +229,7 @@ private fun CrisisCleanupBottomBar( destinations: List, onNavigateToDestination: (TopLevelDestination) -> Unit, currentDestination: NavDestination?, + isAppUpdateAvailable: Boolean, modifier: Modifier = Modifier, ) { CrisisCleanupNavigationBar(modifier = modifier) { @@ -187,6 +237,7 @@ private fun CrisisCleanupBottomBar( destinations = destinations, onNavigateToDestination = onNavigateToDestination, currentDestination = currentDestination, + isAppUpdateAvailable = isAppUpdateAvailable, ) { isSelected: Boolean, title: String, diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index 62940560a..e7f78b8b6 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -162,7 +162,9 @@ private fun BoxScope.LoadedContent( val orgPersistentInvite by viewModel.orgPersistentInvites.collectAsStateWithLifecycle() if (showPasswordReset) { - appState.navController.navigateToPasswordReset(false) + LaunchedEffect(Unit) { + appState.navController.navigateToPasswordReset(false) + } } else if (showMagicLinkLogin) { appState.navController.navigateToMagicLinkLogin() } else if (orgUserInviteCode.isNotBlank()) { @@ -202,10 +204,13 @@ private fun BoxScope.LoadedContent( val menuTutorialStep by viewModel.menuTutorialStep.collectAsStateWithLifecycle() + val isUpdateAvailable by viewModel.isAppUpdateAvailable.collectAsStateWithLifecycle(false) + NavigableContent( snackbarHostState, appState, - isOnboarding, + isAppUpdateAvailable = isUpdateAvailable, + isOnboarding = isOnboarding, menuTutorialStep, viewModel.tutorialViewTracker.viewSizePositionLookup, viewModel::onMenuTutorialNext, @@ -221,7 +226,9 @@ private fun BoxScope.LoadedContent( } if (showPasswordReset) { - appState.navController.navigateToPasswordReset(true) + LaunchedEffect(Unit) { + appState.navController.navigateToPasswordReset(true) + } } } @@ -317,6 +324,7 @@ private fun AcceptTermsContent( private fun NavigableContent( snackbarHostState: SnackbarHostState, appState: CrisisCleanupAppState, + isAppUpdateAvailable: Boolean, isOnboarding: Boolean, menuTutorialStep: TutorialStep, tutorialViewLookup: SnapshotStateMap, @@ -348,6 +356,7 @@ private fun NavigableContent( ) { AppNavigationBar( appState, + isAppUpdateAvailable, navBarSizePositionModifier.testTag("AppNavigationBottomBar"), ) } @@ -378,6 +387,7 @@ private fun NavigableContent( if (showNavigation && !layoutBottomNav) { AppNavigationBar( appState, + isAppUpdateAvailable, navBarSizePositionModifier .safeDrawingPadding() .testTag("AppNavigationSideRail"), diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index e4db9d4b1..70d1a3330 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -21,6 +21,8 @@ dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.datetime) implementation(libs.timeago) + implementation(libs.libphonenumber) testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk.android) } \ No newline at end of file diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/InputValidator.kt b/core/common/src/main/java/com/crisiscleanup/core/common/InputValidator.kt index cfa724856..8b50b5c7b 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/InputValidator.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/InputValidator.kt @@ -1,31 +1,76 @@ package com.crisiscleanup.core.common +import android.content.Context import android.util.Patterns +import dagger.hilt.android.qualifiers.ApplicationContext +import io.michaelrocks.libphonenumber.android.NumberParseException import javax.inject.Inject +import io.michaelrocks.libphonenumber.android.PhoneNumberUtil as LibPhoneNumber +import io.michaelrocks.libphonenumber.android.PhoneNumberUtil.PhoneNumberFormat as PhoneFormat /** * Validates various types of user input */ interface InputValidator { fun validateEmailAddress(emailAddress: String): Boolean - fun validatePhoneNumber(value: String, allowSpaces: Boolean = true): Boolean + fun validatePhoneNumber(value: String, regionCode: String = "US"): PhoneNumberValidation fun hasEmailAddress(text: String): Boolean } -class CommonInputValidator @Inject constructor() : InputValidator { - private val phoneNumbersRegex = """^\+?[\d-]+$""".toRegex() - private val phoneNumbersAndSpacesRegex = """^\+?[\d\s-]+$""".toRegex() +class CommonInputValidator @Inject constructor( + @ApplicationContext context: Context, +) : InputValidator { private val commonEmailRegex = """\b[^@]+@[^.]+\.[A-Za-z]{2,}\b""".toRegex() + private val nonDigitRegex = """\D""".toRegex() + private val phoneUtil by lazy { + LibPhoneNumber.createInstance(context) + } + override fun validateEmailAddress(emailAddress: String) = Patterns.EMAIL_ADDRESS.matcher(emailAddress).matches() - override fun validatePhoneNumber(value: String, allowSpaces: Boolean) = - if (allowSpaces) { - phoneNumbersAndSpacesRegex.matches(value) - } else { - phoneNumbersRegex.matches(value) + override fun validatePhoneNumber(value: String, regionCode: String): PhoneNumberValidation { + var exception: Exception? = null + try { + var phoneNumber = value + if (!value.trim().startsWith("+")) { + val digits = value.trim().replace(nonDigitRegex, "") + if (digits.length != 10) { + phoneNumber = "+$value" + } + } + // TODO Use region from device + phoneUtil.parse(phoneNumber, regionCode)?.let { parsed -> + if (phoneUtil.isValidNumber(parsed)) { + val isUsCountryCode = regionCode == "US" && parsed.countryCode == 1 + val format = if (isUsCountryCode) { + PhoneFormat.NATIONAL + } else { + PhoneFormat.INTERNATIONAL + } + val formatted = phoneUtil.format(parsed, format) + return PhoneNumberValidation( + isValid = true, + formatted = formatted, + ) + } + } + } catch (e: NumberParseException) { + exception = e } + return PhoneNumberValidation( + isValid = false, + formatted = "", + error = exception, + ) + } override fun hasEmailAddress(text: String) = commonEmailRegex.containsMatchIn(text) } + +data class PhoneNumberValidation( + val isValid: Boolean, + val formatted: String, + val error: Exception? = null, +) diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt b/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt index d6de39b54..ed1c9cc14 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt @@ -9,6 +9,9 @@ object PhoneNumberUtil { private val isTenDigitNumberPattern = """^\s*\d{10}\s*$""".toRegex() private val isOneTenDigitNumberPattern = """^\s*1\d{10}\s*$""".toRegex() + private val commonPhoneFormat1Pattern = + """^\+?1?\s?\((\d{3})\)\s*(\d{3})[. -](\d{4})\s*$""".toRegex() + private val inParenthesisPattern = """\((\d{3})\)""".toRegex() private val leadingOnePattern = """(?:^|\b)\+?1\s""".toRegex() private val compact334DelimiterPattern = @@ -59,6 +62,12 @@ object PhoneNumberUtil { return null } + commonPhoneFormat1Pattern.matchEntire(raw)?.let { + with(it.groupValues) { + return singleParsedNumber("${get(1)}${get(2)}${get(3)}") + } + } + val unparenthesized = raw.replace(inParenthesisPattern, "$1") val leadingOneTrimmed = unparenthesized.replace(leadingOnePattern, "") val threeThreeFourUndelimited = diff --git a/core/common/src/test/java/com/crisiscleanup/core/common/InputValidatorTest.kt b/core/common/src/test/java/com/crisiscleanup/core/common/InputValidatorTest.kt index 0837dca3d..361b4cc36 100644 --- a/core/common/src/test/java/com/crisiscleanup/core/common/InputValidatorTest.kt +++ b/core/common/src/test/java/com/crisiscleanup/core/common/InputValidatorTest.kt @@ -1,16 +1,20 @@ package com.crisiscleanup.core.common +import android.content.Context +import io.mockk.impl.annotations.MockK import org.junit.Before import org.junit.Test import kotlin.test.assertFalse import kotlin.test.assertTrue class InputValidatorTest { + @MockK + lateinit var context: Context private lateinit var inputValidator: InputValidator @Before fun setup() { - inputValidator = CommonInputValidator() + inputValidator = CommonInputValidator(context) } @Test diff --git a/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt b/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt index 11186cf35..9c88eef57 100644 --- a/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt +++ b/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt @@ -47,6 +47,25 @@ class PhoneNumberUtilTest { } } + @Test + fun commonFormats() { + val inputs = listOf( + "(234) 567-8901", + "(234) 567.8901", + "(234) 567 8901", + "1(234) 567-8901", + "1(234) 567.8901", + "1 (234) 567 8901", + "+1(234) 567-8901", + "+1 (234) 567.8901", + "+1 (234) 567 8901", + ) + for (input in inputs) { + val actual = PhoneNumberUtil.parsePhoneNumbers(input)?.parsedNumbers + assertEquals(listOf("2345678901"), actual) + } + } + @Test fun compact334() { val inputs = listOf( diff --git a/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/IncidentHeaderView.kt b/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/IncidentHeaderView.kt index 870546e9f..c0a7b2805 100644 --- a/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/IncidentHeaderView.kt +++ b/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/IncidentHeaderView.kt @@ -66,7 +66,7 @@ fun IncidentHeaderView( } else if (isPendingSync) { CrisisCleanupIconButton( onClick = scheduleSync, - imageVector = CrisisCleanupIcons.Cloud, + imageVector = CrisisCleanupIcons.CloudOff, contentDescription = t("info.is_pending_sync"), tint = primaryOrangeColor, modifier = Modifier.testTag("caseViewIsPendingSyncIconBtn"), diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt index 67bf4e08a..788cb6ef2 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt @@ -37,7 +37,9 @@ fun NetworkWorksiteFull.asEntity() = WorksiteEntity( longitude = location.coordinates[0], name = name, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, phoneSearch = searchablePhoneNumbers(phone1, phone2), plusCode = plusCode, postalCode = postalCode ?: "", @@ -46,9 +48,9 @@ fun NetworkWorksiteFull.asEntity() = WorksiteEntity( svi = svi, what3Words = what3words, updatedAt = updatedAt, - photoCount = files.map(NetworkFile::mimeContentType) - .filter { it?.startsWith("image/") == true } - .size, + photoCount = files?.map(NetworkFile::mimeContentType) + ?.filter { it?.startsWith("image/") == true } + ?.size, ) // Copy similar changes from [NetworkWorksiteFull.asEntity] above @@ -71,7 +73,9 @@ fun NetworkWorksiteCoreData.asEntity() = WorksiteEntity( longitude = location.coordinates[0], name = name, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, phoneSearch = searchablePhoneNumbers(phone1, phone2), plusCode = plusCode, postalCode = postalCode ?: "", @@ -108,7 +112,9 @@ fun NetworkWorksiteShort.asEntity() = WorksiteEntity( autoContactFrequencyT = null, email = null, phone1 = null, + phone1Notes = null, phone2 = null, + phone2Notes = null, phoneSearch = null, plusCode = null, reportedBy = null, @@ -141,7 +147,9 @@ fun NetworkWorksitePage.asEntity() = WorksiteEntity( autoContactFrequencyT = autoContactFrequencyT, email = email, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, phoneSearch = searchablePhoneNumbers(phone1, phone2), plusCode = plusCode, reportedBy = reportedBy, @@ -187,7 +195,7 @@ fun NetworkWorksiteFull.asEntities(): WorksiteEntities { val formData = formData.map(KeyDynamicValuePair::asWorksiteEntity) val flags = flags.map(NetworkFlag::asEntity) val notes = notes.map(NetworkNote::asEntity) - val files = files.map(NetworkFile::asEntity) + val files = files?.map(NetworkFile::asEntity) ?: emptyList() return WorksiteEntities( core, flags, diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AccountDataRefresher.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AccountDataRefresher.kt index c7fcf95d6..dfdbec8a6 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AccountDataRefresher.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AccountDataRefresher.kt @@ -44,16 +44,18 @@ class AccountDataRefresher @Inject constructor( } logger.logCapture("Syncing $syncTag") + + val accountId = accountDataRepository.accountData.first().id try { - val profile = networkDataSource.getProfileData() - if (profile.organization.isActive == false) { + val profile = networkDataSource.getProfileData(accountId) + if (profile.organization?.isActive == false) { accountEventBus.onAccountInactiveOrganization(dataSource.accountData.first().id) } else if (profile.hasAcceptedTerms != null) { dataSource.update( profile.files?.profilePictureUrl, profile.hasAcceptedTerms!!, profile.approvedIncidents!!, - profile.activeRoles, + profile.activeRoles!!, ) accountDataUpdateTime = Clock.System.now() diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppMetricsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppMetricsRepository.kt index 3f418b37a..7b7166ea0 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppMetricsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppMetricsRepository.kt @@ -1,6 +1,7 @@ package com.crisiscleanup.core.data.repository import com.crisiscleanup.core.common.AppEnv +import com.crisiscleanup.core.common.AppVersionProvider import com.crisiscleanup.core.common.di.ApplicationScope import com.crisiscleanup.core.common.log.AppLogger import com.crisiscleanup.core.common.log.CrisisCleanupLoggers @@ -15,6 +16,7 @@ import com.crisiscleanup.core.network.appsupport.AppSupportClient import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -24,6 +26,7 @@ import javax.inject.Singleton // TODO Rename to AppInfoRepository interface LocalAppMetricsRepository { val metrics: Flow + val isAppUpdateAvailable: Flow suspend fun setEarlybirdEnd(end: BuildEndOfLife) @@ -39,6 +42,7 @@ interface LocalAppMetricsRepository { class AppMetricsRepository @Inject constructor( private val dataSource: LocalAppMetricsDataSource, private val appSupportNetworkDataSource: AppSupportClient, + appVersionProvider: AppVersionProvider, private val appEnv: AppEnv, @ApplicationScope private val externalScope: CoroutineScope, @Dispatcher(CrisisCleanupDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, @@ -46,6 +50,10 @@ class AppMetricsRepository @Inject constructor( ) : LocalAppMetricsRepository { override val metrics: Flow = dataSource.metrics + override val isAppUpdateAvailable = metrics.mapLatest { + appVersionProvider.versionCode < it.appPublishedVersion + } + override suspend fun setEarlybirdEnd(end: BuildEndOfLife) { dataSource.setEarlybirdEnd(end) } @@ -60,13 +68,14 @@ class AppMetricsRepository @Inject constructor( try { appSupportNetworkDataSource.getAppSupportInfo(appEnv.isNotProduction) ?.let { info -> - dataSource.setMinSupportedAppVersion( + dataSource.setAppVersions( MinSupportedAppVersion( minBuild = info.minBuildVersion, title = info.title ?: "", message = info.message, link = info.link ?: "", ), + publishedVersion = info.publishedVersion ?: 0, ) } } catch (e: Exception) { diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksiteChangeRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksiteChangeRepository.kt index bf69cbf4c..bddb3202e 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksiteChangeRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksiteChangeRepository.kt @@ -421,11 +421,13 @@ class CrisisCleanupWorksiteChangeRepository @Inject constructor( private suspend fun syncPhotoChanges(worksiteId: Long) { try { - val (networkWorksiteId, deleteFileIds) = - localImageDaoPlus.getDeletedPhotoNetworkFileIds(worksiteId) - if (deleteFileIds.isNotEmpty()) { - worksitePhotoChangeSyncer.deletePhotoFiles(networkWorksiteId, deleteFileIds) - syncLogger.log("Deleted photos", deleteFileIds.joinToString(", ")) + val worksiteFileIds = localImageDaoPlus.getDeletedPhotoNetworkFileIds(worksiteId) + with(worksiteFileIds) { + if (fileIds.isNotEmpty()) { + val networkWorksiteId = worksiteDao.getWorksiteNetworkId(worksiteId) + worksitePhotoChangeSyncer.deletePhotoFiles(networkWorksiteId, fileIds) + syncLogger.log("Deleted photos", fileIds.joinToString(", ")) + } } } catch (e: Exception) { syncLogger.log("Delete photo error", e.message ?: "") diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index ab71b267f..744330632 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -37,5 +37,8 @@ dependencies { androidTestImplementation(projects.core.testing) androidTestImplementation(libs.room.testing) - androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.mockk.android) { + exclude(group = "net.bytebuddy", module = "byte-buddy") + exclude(group = "net.bytebuddy", module = "byte-buddy-agent") + } } \ No newline at end of file diff --git a/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/47.json b/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/47.json new file mode 100644 index 000000000..bb78dd869 --- /dev/null +++ b/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/47.json @@ -0,0 +1,3291 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "a81b1821c83674f05be426c781fa7805", + "entities": [ + { + "tableName": "work_type_statuses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`status` TEXT NOT NULL, `name` TEXT NOT NULL, `list_order` INTEGER NOT NULL, `primary_state` TEXT NOT NULL, PRIMARY KEY(`status`))", + "fields": [ + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryState", + "columnName": "primary_state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "status" + ] + }, + "indices": [ + { + "name": "index_work_type_statuses_list_order", + "unique": false, + "columnNames": [ + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_type_statuses_list_order` ON `${TABLE_NAME}` (`list_order`)" + } + ] + }, + { + "tableName": "incidents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `start_at` INTEGER NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL DEFAULT '', `case_label` TEXT NOT NULL DEFAULT '', `incident_type` TEXT NOT NULL DEFAULT '', `active_phone_number` TEXT DEFAULT '', `turn_on_release` INTEGER NOT NULL DEFAULT 0, `is_archived` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startAt", + "columnName": "start_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "caseLabel", + "columnName": "case_label", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "incident_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "activePhoneNumber", + "columnName": "active_phone_number", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "turnOnRelease", + "columnName": "turn_on_release", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isArchived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "idx_newest_to_oldest_incidents", + "unique": false, + "columnNames": [ + "start_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_newest_to_oldest_incidents` ON `${TABLE_NAME}` (`start_at` DESC)" + } + ] + }, + { + "tableName": "incident_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `location` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "incident_to_incident_location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `incident_location_id` INTEGER NOT NULL, PRIMARY KEY(`incident_id`, `incident_location_id`), FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`incident_location_id`) REFERENCES `incident_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentLocationId", + "columnName": "incident_location_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id", + "incident_location_id" + ] + }, + "indices": [ + { + "name": "idx_incident_location_to_incident", + "unique": false, + "columnNames": [ + "incident_location_id", + "incident_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_incident_location_to_incident` ON `${TABLE_NAME}` (`incident_location_id`, `incident_id`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "incident_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_location_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_form_fields", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `label` TEXT NOT NULL, `html_type` TEXT NOT NULL, `data_group` TEXT NOT NULL, `help` TEXT DEFAULT '', `placeholder` TEXT DEFAULT '', `read_only_break_glass` INTEGER NOT NULL, `values_default_json` TEXT DEFAULT '', `is_checkbox_default_true` INTEGER DEFAULT 0, `order_label` INTEGER NOT NULL DEFAULT -1, `validation` TEXT DEFAULT '', `recur_default` TEXT DEFAULT '0', `values_json` TEXT DEFAULT '', `is_required` INTEGER DEFAULT 0, `is_read_only` INTEGER DEFAULT 0, `list_order` INTEGER NOT NULL, `is_invalidated` INTEGER NOT NULL, `field_key` TEXT NOT NULL, `field_parent_key` TEXT DEFAULT '', `parent_key` TEXT NOT NULL DEFAULT '', `selected_toggle_work_type` TEXT DEFAULT '', PRIMARY KEY(`incident_id`, `parent_key`, `field_key`), FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlType", + "columnName": "html_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataGroup", + "columnName": "data_group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "help", + "columnName": "help", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "placeholder", + "columnName": "placeholder", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "readOnlyBreakGlass", + "columnName": "read_only_break_glass", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valuesDefaultJson", + "columnName": "values_default_json", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "isCheckboxDefaultTrue", + "columnName": "is_checkbox_default_true", + "affinity": "INTEGER", + "defaultValue": "0" + }, + { + "fieldPath": "orderLabel", + "columnName": "order_label", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "validation", + "columnName": "validation", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "recurDefault", + "columnName": "recur_default", + "affinity": "TEXT", + "defaultValue": "'0'" + }, + { + "fieldPath": "valuesJson", + "columnName": "values_json", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "isRequired", + "columnName": "is_required", + "affinity": "INTEGER", + "defaultValue": "0" + }, + { + "fieldPath": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER", + "defaultValue": "0" + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInvalidated", + "columnName": "is_invalidated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fieldKey", + "columnName": "field_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fieldParentKey", + "columnName": "field_parent_key", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "parentKeyNonNull", + "columnName": "parent_key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "selectToggleWorkType", + "columnName": "selected_toggle_work_type", + "affinity": "TEXT", + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id", + "parent_key", + "field_key" + ] + }, + "indices": [ + { + "name": "index_incident_form_fields_data_group_parent_key_list_order", + "unique": false, + "columnNames": [ + "data_group", + "parent_key", + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_incident_form_fields_data_group_parent_key_list_order` ON `${TABLE_NAME}` (`data_group`, `parent_key`, `list_order`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `shape_type` TEXT NOT NULL DEFAULT '', `coordinates` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shapeType", + "columnName": "shape_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "coordinates", + "columnName": "coordinates", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "worksite_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `sync_start` INTEGER NOT NULL DEFAULT 0, `target_count` INTEGER NOT NULL, `paged_count` INTEGER NOT NULL DEFAULT 0, `successful_sync` INTEGER, `attempted_sync` INTEGER, `attempted_counter` INTEGER NOT NULL, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`))", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStart", + "columnName": "sync_start", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pagedCount", + "columnName": "paged_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER" + }, + { + "fieldPath": "attemptedSync", + "columnName": "attempted_sync", + "affinity": "INTEGER" + }, + { + "fieldPath": "attemptedCounter", + "columnName": "attempted_counter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + } + }, + { + "tableName": "worksites_root", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_uuid` TEXT NOT NULL DEFAULT '', `local_modified_at` INTEGER NOT NULL DEFAULT 0, `synced_at` INTEGER NOT NULL DEFAULT 0, `local_global_uuid` TEXT NOT NULL DEFAULT '', `is_local_modified` INTEGER NOT NULL DEFAULT 0, `sync_attempt` INTEGER NOT NULL DEFAULT 0, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncUuid", + "columnName": "sync_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "localModifiedAt", + "columnName": "local_modified_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isLocalModified", + "columnName": "is_local_modified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncAttempt", + "columnName": "sync_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksites_root_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksites_root_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_worksites_root_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_root_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_worksites_root_is_local_modified_local_modified_at", + "unique": false, + "columnNames": [ + "is_local_modified", + "local_modified_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_root_is_local_modified_local_modified_at` ON `${TABLE_NAME}` (`is_local_modified` DESC, `local_modified_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, `address` TEXT NOT NULL, `auto_contact_frequency_t` TEXT, `case_number` TEXT NOT NULL, `case_number_order` INTEGER NOT NULL DEFAULT 0, `city` TEXT NOT NULL, `county` TEXT NOT NULL, `created_at` INTEGER, `email` TEXT DEFAULT '', `favorite_id` INTEGER, `key_work_type_type` TEXT NOT NULL DEFAULT '', `key_work_type_org` INTEGER, `key_work_type_status` TEXT NOT NULL DEFAULT '', `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `name` TEXT NOT NULL, `phone1` TEXT, `phone1_notes` TEXT DEFAULT '', `phone2` TEXT DEFAULT '', `phone2_notes` TEXT DEFAULT '', `phone_search` TEXT DEFAULT '', `plus_code` TEXT DEFAULT '', `postal_code` TEXT NOT NULL, `reported_by` INTEGER, `state` TEXT NOT NULL, `svi` REAL, `what3Words` TEXT DEFAULT '', `updated_at` INTEGER NOT NULL, `network_photo_count` INTEGER DEFAULT 0, `is_local_favorite` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `worksites_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autoContactFrequencyT", + "columnName": "auto_contact_frequency_t", + "affinity": "TEXT" + }, + { + "fieldPath": "caseNumber", + "columnName": "case_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseNumberOrder", + "columnName": "case_number_order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "county", + "columnName": "county", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "favoriteId", + "columnName": "favorite_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "keyWorkTypeType", + "columnName": "key_work_type_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "keyWorkTypeOrgClaim", + "columnName": "key_work_type_org", + "affinity": "INTEGER" + }, + { + "fieldPath": "keyWorkTypeStatus", + "columnName": "key_work_type_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone1", + "columnName": "phone1", + "affinity": "TEXT" + }, + { + "fieldPath": "phone1Notes", + "columnName": "phone1_notes", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "phone2", + "columnName": "phone2", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "phone2Notes", + "columnName": "phone2_notes", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "phoneSearch", + "columnName": "phone_search", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "plusCode", + "columnName": "plus_code", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "postalCode", + "columnName": "postal_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reportedBy", + "columnName": "reported_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "svi", + "columnName": "svi", + "affinity": "REAL" + }, + { + "fieldPath": "what3Words", + "columnName": "what3Words", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "photoCount", + "columnName": "network_photo_count", + "affinity": "INTEGER", + "defaultValue": "0" + }, + { + "fieldPath": "isLocalFavorite", + "columnName": "is_local_favorite", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksites_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_worksites_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_worksites_incident_id_latitude_longitude", + "unique": false, + "columnNames": [ + "incident_id", + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_latitude_longitude` ON `${TABLE_NAME}` (`incident_id`, `latitude`, `longitude`)" + }, + { + "name": "index_worksites_incident_id_longitude_latitude", + "unique": false, + "columnNames": [ + "incident_id", + "longitude", + "latitude" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_longitude_latitude` ON `${TABLE_NAME}` (`incident_id`, `longitude`, `latitude`)" + }, + { + "name": "index_worksites_incident_id_svi", + "unique": false, + "columnNames": [ + "incident_id", + "svi" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_svi` ON `${TABLE_NAME}` (`incident_id`, `svi`)" + }, + { + "name": "index_worksites_incident_id_updated_at", + "unique": false, + "columnNames": [ + "incident_id", + "updated_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_updated_at` ON `${TABLE_NAME}` (`incident_id`, `updated_at`)" + }, + { + "name": "index_worksites_incident_id_created_at", + "unique": false, + "columnNames": [ + "incident_id", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_created_at` ON `${TABLE_NAME}` (`incident_id`, `created_at`)" + }, + { + "name": "index_worksites_incident_id_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_case_number` ON `${TABLE_NAME}` (`incident_id`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_name_county_city_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "name", + "county", + "city", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_name_county_city_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `name`, `county`, `city`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_city_name_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "city", + "name", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_city_name_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `city`, `name`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_county_name_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "county", + "name", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_county_name_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `county`, `name`, `case_number_order`, `case_number`)" + } + ], + "foreignKeys": [ + { + "table": "worksites_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "work_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER, `claimed_by` INTEGER, `next_recur_at` INTEGER, `phase` INTEGER, `recur` TEXT, `status` TEXT NOT NULL, `work_type` TEXT NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "orgClaim", + "columnName": "claimed_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "nextRecurAt", + "columnName": "next_recur_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "phase", + "columnName": "phase", + "affinity": "INTEGER" + }, + { + "fieldPath": "recur", + "columnName": "recur", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workType", + "columnName": "work_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_work_type", + "unique": true, + "columnNames": [ + "worksite_id", + "work_type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_work_type` ON `${TABLE_NAME}` (`worksite_id`, `work_type`)" + }, + { + "name": "index_work_types_worksite_id_network_id", + "unique": false, + "columnNames": [ + "worksite_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_worksite_id_network_id` ON `${TABLE_NAME}` (`worksite_id`, `network_id`)" + }, + { + "name": "index_work_types_status_worksite_id", + "unique": false, + "columnNames": [ + "status", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_status_worksite_id` ON `${TABLE_NAME}` (`status`, `worksite_id`)" + }, + { + "name": "index_work_types_claimed_by_worksite_id", + "unique": false, + "columnNames": [ + "claimed_by", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_claimed_by_worksite_id` ON `${TABLE_NAME}` (`claimed_by`, `worksite_id`)" + }, + { + "name": "index_work_types_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_network_id` ON `${TABLE_NAME}` (`network_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_form_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`worksite_id` INTEGER NOT NULL, `field_key` TEXT NOT NULL, `is_bool_value` INTEGER NOT NULL, `value_string` TEXT NOT NULL, `value_bool` INTEGER NOT NULL, PRIMARY KEY(`worksite_id`, `field_key`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fieldKey", + "columnName": "field_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isBoolValue", + "columnName": "is_bool_value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueString", + "columnName": "value_string", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueBool", + "columnName": "value_bool", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "worksite_id", + "field_key" + ] + }, + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_flags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `action` TEXT, `created_at` INTEGER NOT NULL, `is_high_priority` INTEGER DEFAULT 0, `notes` TEXT DEFAULT '', `reason_t` TEXT NOT NULL, `requested_action` TEXT DEFAULT '', FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHighPriority", + "columnName": "is_high_priority", + "affinity": "INTEGER", + "defaultValue": "0" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "reasonT", + "columnName": "reason_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestedAction", + "columnName": "requested_action", + "affinity": "TEXT", + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_flag", + "unique": true, + "columnNames": [ + "worksite_id", + "reason_t" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_flag` ON `${TABLE_NAME}` (`worksite_id`, `reason_t`)" + }, + { + "name": "index_worksite_flags_reason_t", + "unique": false, + "columnNames": [ + "reason_t" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_flags_reason_t` ON `${TABLE_NAME}` (`reason_t`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `local_global_uuid` TEXT NOT NULL DEFAULT '', `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `is_survivor` INTEGER NOT NULL, `note` TEXT NOT NULL DEFAULT '', FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSurvivor", + "columnName": "is_survivor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_note", + "unique": true, + "columnNames": [ + "worksite_id", + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_note` ON `${TABLE_NAME}` (`worksite_id`, `network_id`, `local_global_uuid`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "language_translations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `name` TEXT NOT NULL, `translations_json` TEXT, `synced_at` INTEGER DEFAULT 0, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "translationsJson", + "columnName": "translations_json", + "affinity": "TEXT" + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "sync_logs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `log_time` INTEGER NOT NULL, `log_type` TEXT NOT NULL DEFAULT '', `message` TEXT NOT NULL, `details` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logTime", + "columnName": "log_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logType", + "columnName": "log_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "details", + "columnName": "details", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sync_logs_log_time", + "unique": false, + "columnNames": [ + "log_time" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sync_logs_log_time` ON `${TABLE_NAME}` (`log_time` DESC)" + } + ] + }, + { + "tableName": "worksite_changes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `app_version` INTEGER NOT NULL, `organization_id` INTEGER NOT NULL, `worksite_id` INTEGER NOT NULL, `sync_uuid` TEXT NOT NULL DEFAULT '', `change_model_version` INTEGER NOT NULL, `change_data` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `save_attempt` INTEGER NOT NULL DEFAULT 0, `archive_action` TEXT NOT NULL, `save_attempt_at` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`worksite_id`) REFERENCES `worksites_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appVersion", + "columnName": "app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncUuid", + "columnName": "sync_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "changeModelVersion", + "columnName": "change_model_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changeData", + "columnName": "change_data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saveAttempt", + "columnName": "save_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "archiveAction", + "columnName": "archive_action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saveAttemptAt", + "columnName": "save_attempt_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_changes_worksite_id_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_created_at` ON `${TABLE_NAME}` (`worksite_id`, `created_at`)" + }, + { + "name": "index_worksite_changes_worksite_id_save_attempt", + "unique": false, + "columnNames": [ + "worksite_id", + "save_attempt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_save_attempt` ON `${TABLE_NAME}` (`worksite_id`, `save_attempt`)" + }, + { + "name": "index_worksite_changes_worksite_id_save_attempt_at_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "save_attempt_at", + "created_at" + ], + "orders": [ + "ASC", + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_save_attempt_at_created_at` ON `${TABLE_NAME}` (`worksite_id` ASC, `save_attempt_at` ASC, `created_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "worksites_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_organizations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `primary_location` INTEGER, `secondary_location` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryLocation", + "columnName": "primary_location", + "affinity": "INTEGER" + }, + { + "fieldPath": "secondaryLocation", + "columnName": "secondary_location", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "person_contacts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `email` TEXT NOT NULL, `mobile` TEXT NOT NULL, `profilePictureUri` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobile", + "columnName": "mobile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUri", + "columnName": "profilePictureUri", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "organization_to_primary_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`organization_id` INTEGER NOT NULL, `contact_id` INTEGER NOT NULL, PRIMARY KEY(`organization_id`, `contact_id`), FOREIGN KEY(`organization_id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contact_id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contact_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "organization_id", + "contact_id" + ] + }, + "indices": [ + { + "name": "idx_contact_to_organization", + "unique": false, + "columnNames": [ + "contact_id", + "organization_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_contact_to_organization` ON `${TABLE_NAME}` (`contact_id`, `organization_id`)" + } + ], + "foreignKeys": [ + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "organization_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contact_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "organization_to_affiliate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `affiliate_id` INTEGER NOT NULL, PRIMARY KEY(`id`, `affiliate_id`), FOREIGN KEY(`id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "affiliateId", + "columnName": "affiliate_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "affiliate_id" + ] + }, + "indices": [ + { + "name": "index_organization_to_affiliate_affiliate_id_id", + "unique": false, + "columnNames": [ + "affiliate_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_organization_to_affiliate_affiliate_id_id` ON `${TABLE_NAME}` (`affiliate_id`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_organization_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `target_count` INTEGER NOT NULL, `successful_sync` INTEGER, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`))", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER" + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + } + }, + { + "tableName": "recent_worksites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `incident_id` INTEGER NOT NULL, `viewed_at` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewedAt", + "columnName": "viewed_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_recent_worksites_incident_id_viewed_at", + "unique": false, + "columnNames": [ + "incident_id", + "viewed_at" + ], + "orders": [ + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recent_worksites_incident_id_viewed_at` ON `${TABLE_NAME}` (`incident_id` ASC, `viewed_at` DESC)" + }, + { + "name": "index_recent_worksites_viewed_at", + "unique": false, + "columnNames": [ + "viewed_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recent_worksites_viewed_at` ON `${TABLE_NAME}` (`viewed_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_work_type_requests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `work_type` TEXT NOT NULL, `reason` TEXT NOT NULL, `by_org` INTEGER NOT NULL, `to_org` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `approved_at` INTEGER, `rejected_at` INTEGER, `approved_rejected_reason` TEXT NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workType", + "columnName": "work_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "byOrg", + "columnName": "by_org", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "toOrg", + "columnName": "to_org", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "approvedAt", + "columnName": "approved_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "rejectedAt", + "columnName": "rejected_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "approvedRejectedReason", + "columnName": "approved_rejected_reason", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_work_type_requests_worksite_id_work_type_by_org", + "unique": true, + "columnNames": [ + "worksite_id", + "work_type", + "by_org" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksite_work_type_requests_worksite_id_work_type_by_org` ON `${TABLE_NAME}` (`worksite_id`, `work_type`, `by_org`)" + }, + { + "name": "index_worksite_work_type_requests_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_work_type_requests_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_worksite_work_type_requests_worksite_id_by_org", + "unique": false, + "columnNames": [ + "worksite_id", + "by_org" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_work_type_requests_worksite_id_by_org` ON `${TABLE_NAME}` (`worksite_id`, `by_org`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "network_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `file_id` INTEGER NOT NULL DEFAULT 0, `file_type_t` TEXT NOT NULL, `full_url` TEXT, `large_thumbnail_url` TEXT, `mime_content_type` TEXT NOT NULL, `small_thumbnail_url` TEXT, `tag` TEXT, `title` TEXT, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fileTypeT", + "columnName": "file_type_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullUrl", + "columnName": "full_url", + "affinity": "TEXT" + }, + { + "fieldPath": "largeThumbnailUrl", + "columnName": "large_thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "mimeContentType", + "columnName": "mime_content_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smallThumbnailUrl", + "columnName": "small_thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "worksite_to_network_file", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`worksite_id` INTEGER NOT NULL, `network_file_id` INTEGER NOT NULL, PRIMARY KEY(`worksite_id`, `network_file_id`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`network_file_id`) REFERENCES `network_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkFileId", + "columnName": "network_file_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "worksite_id", + "network_file_id" + ] + }, + "indices": [ + { + "name": "index_worksite_to_network_file_network_file_id_worksite_id", + "unique": false, + "columnNames": [ + "network_file_id", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_to_network_file_network_file_id_worksite_id` ON `${TABLE_NAME}` (`network_file_id`, `worksite_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "network_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "network_file_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "network_file_local_images", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `is_deleted` INTEGER NOT NULL, `rotate_degrees` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `network_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "is_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rotateDegrees", + "columnName": "rotate_degrees", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_network_file_local_images_is_deleted", + "unique": false, + "columnNames": [ + "is_deleted" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_network_file_local_images_is_deleted` ON `${TABLE_NAME}` (`is_deleted`)" + } + ], + "foreignKeys": [ + { + "table": "network_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_local_images", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `worksite_id` INTEGER NOT NULL, `local_document_id` TEXT NOT NULL, `uri` TEXT NOT NULL, `tag` TEXT NOT NULL, `rotate_degrees` INTEGER NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentId", + "columnName": "local_document_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rotateDegrees", + "columnName": "rotate_degrees", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_local_images_worksite_id_local_document_id", + "unique": true, + "columnNames": [ + "worksite_id", + "local_document_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksite_local_images_worksite_id_local_document_id` ON `${TABLE_NAME}` (`worksite_id`, `local_document_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_worksites_full_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `synced_at` INTEGER, `center_my_location` INTEGER NOT NULL, `center_latitude` REAL NOT NULL DEFAULT 999, `center_longitude` REAL NOT NULL DEFAULT 999, `query_area_radius` REAL NOT NULL, PRIMARY KEY(`incident_id`), FOREIGN KEY(`incident_id`) REFERENCES `worksite_sync_stats`(`incident_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "isMyLocationCentered", + "columnName": "center_my_location", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "center_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "999" + }, + { + "fieldPath": "longitude", + "columnName": "center_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "999" + }, + { + "fieldPath": "radius", + "columnName": "query_area_radius", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "foreignKeys": [ + { + "table": "worksite_sync_stats", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "incident_id" + ] + } + ] + }, + { + "tableName": "incident_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT NOT NULL, `short_name` TEXT NOT NULL DEFAULT '', `incident_type` TEXT NOT NULL DEFAULT '', content=`incidents`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "incident_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "incidents", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_BEFORE_UPDATE BEFORE UPDATE ON `incidents` BEGIN DELETE FROM `incident_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_BEFORE_DELETE BEFORE DELETE ON `incidents` BEGIN DELETE FROM `incident_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_AFTER_UPDATE AFTER UPDATE ON `incidents` BEGIN INSERT INTO `incident_fts`(`docid`, `name`, `short_name`, `incident_type`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`short_name`, NEW.`incident_type`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_AFTER_INSERT AFTER INSERT ON `incidents` BEGIN INSERT INTO `incident_fts`(`docid`, `name`, `short_name`, `incident_type`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`short_name`, NEW.`incident_type`); END" + ] + }, + { + "tableName": "incident_organization_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT NOT NULL, content=`incident_organizations`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "incident_organizations", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_BEFORE_UPDATE BEFORE UPDATE ON `incident_organizations` BEGIN DELETE FROM `incident_organization_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_BEFORE_DELETE BEFORE DELETE ON `incident_organizations` BEGIN DELETE FROM `incident_organization_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_AFTER_UPDATE AFTER UPDATE ON `incident_organizations` BEGIN INSERT INTO `incident_organization_fts`(`docid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_AFTER_INSERT AFTER INSERT ON `incident_organizations` BEGIN INSERT INTO `incident_organization_fts`(`docid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END" + ] + }, + { + "tableName": "case_history_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `created_by` INTEGER NOT NULL, `event_key` TEXT NOT NULL, `past_tense_t` TEXT NOT NULL, `actor_location_name` TEXT NOT NULL, `recipient_location_name` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventKey", + "columnName": "event_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pastTenseT", + "columnName": "past_tense_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorLocationName", + "columnName": "actor_location_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientLocationName", + "columnName": "recipient_location_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_case_history_events_worksite_id_created_by_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "created_by", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_case_history_events_worksite_id_created_by_created_at` ON `${TABLE_NAME}` (`worksite_id`, `created_by`, `created_at`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "case_history_event_attrs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `incident_name` TEXT NOT NULL, `patient_case_number` TEXT, `patient_id` INTEGER NOT NULL, `patient_label_t` TEXT, `patient_location_name` TEXT, `patient_name_t` TEXT, `patient_reason_t` TEXT, `patient_status_name_t` TEXT, `recipient_case_number` TEXT, `recipient_id` INTEGER, `recipient_name` TEXT, `recipient_name_t` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `case_history_events`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentName", + "columnName": "incident_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientCaseNumber", + "columnName": "patient_case_number", + "affinity": "TEXT" + }, + { + "fieldPath": "patientId", + "columnName": "patient_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patientLabelT", + "columnName": "patient_label_t", + "affinity": "TEXT" + }, + { + "fieldPath": "patientLocationName", + "columnName": "patient_location_name", + "affinity": "TEXT" + }, + { + "fieldPath": "patientNameT", + "columnName": "patient_name_t", + "affinity": "TEXT" + }, + { + "fieldPath": "patientReasonT", + "columnName": "patient_reason_t", + "affinity": "TEXT" + }, + { + "fieldPath": "patientStatusNameT", + "columnName": "patient_status_name_t", + "affinity": "TEXT" + }, + { + "fieldPath": "recipientCaseNumber", + "columnName": "recipient_case_number", + "affinity": "TEXT" + }, + { + "fieldPath": "recipientId", + "columnName": "recipient_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "recipientName", + "columnName": "recipient_name", + "affinity": "TEXT" + }, + { + "fieldPath": "recipientNameT", + "columnName": "recipient_name_t", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "case_history_events", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "person_to_organization", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `organization_id` INTEGER NOT NULL, PRIMARY KEY(`id`, `organization_id`), FOREIGN KEY(`id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`organization_id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "organization_id" + ] + }, + "indices": [ + { + "name": "index_person_to_organization_organization_id_id", + "unique": false, + "columnNames": [ + "organization_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_person_to_organization_organization_id_id` ON `${TABLE_NAME}` (`organization_id`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "organization_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_text_fts_c", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`address` TEXT NOT NULL, `case_number` TEXT NOT NULL, `city` TEXT NOT NULL, `county` TEXT NOT NULL, `email` TEXT NOT NULL, `name` TEXT NOT NULL, `phone_search` TEXT NOT NULL DEFAULT '', content=`worksites`)", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseNumber", + "columnName": "case_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "county", + "columnName": "county", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneSearch", + "columnName": "phone_search", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "worksites", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_c_BEFORE_UPDATE BEFORE UPDATE ON `worksites` BEGIN DELETE FROM `worksite_text_fts_c` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_c_BEFORE_DELETE BEFORE DELETE ON `worksites` BEGIN DELETE FROM `worksite_text_fts_c` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_c_AFTER_UPDATE AFTER UPDATE ON `worksites` BEGIN INSERT INTO `worksite_text_fts_c`(`docid`, `address`, `case_number`, `city`, `county`, `email`, `name`, `phone_search`) VALUES (NEW.`rowid`, NEW.`address`, NEW.`case_number`, NEW.`city`, NEW.`county`, NEW.`email`, NEW.`name`, NEW.`phone_search`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_c_AFTER_INSERT AFTER INSERT ON `worksites` BEGIN INSERT INTO `worksite_text_fts_c`(`docid`, `address`, `case_number`, `city`, `county`, `email`, `name`, `phone_search`) VALUES (NEW.`rowid`, NEW.`address`, NEW.`case_number`, NEW.`city`, NEW.`county`, NEW.`email`, NEW.`name`, NEW.`phone_search`); END" + ] + }, + { + "tableName": "incident_worksites_secondary_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `sync_start` INTEGER NOT NULL DEFAULT 0, `target_count` INTEGER NOT NULL, `paged_count` INTEGER NOT NULL DEFAULT 0, `successful_sync` INTEGER, `attempted_sync` INTEGER, `attempted_counter` INTEGER NOT NULL, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`), FOREIGN KEY(`incident_id`) REFERENCES `worksite_sync_stats`(`incident_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStart", + "columnName": "sync_start", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pagedCount", + "columnName": "paged_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER" + }, + { + "fieldPath": "attemptedSync", + "columnName": "attempted_sync", + "affinity": "INTEGER" + }, + { + "fieldPath": "attemptedCounter", + "columnName": "attempted_counter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "foreignKeys": [ + { + "table": "worksite_sync_stats", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "incident_id" + ] + } + ] + }, + { + "tableName": "lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `local_global_uuid` TEXT NOT NULL DEFAULT '', `created_by` INTEGER, `updated_by` INTEGER, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `parent` INTEGER, `name` TEXT NOT NULL, `description` TEXT, `list_order` INTEGER, `tags` TEXT, `model` TEXT NOT NULL, `object_ids` TEXT NOT NULL DEFAULT '', `shared` TEXT NOT NULL, `permissions` TEXT NOT NULL, `incident_id` INTEGER, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "updatedBy", + "columnName": "updated_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectIds", + "columnName": "object_ids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "shared", + "columnName": "shared", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_lists_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_lists_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_lists_incident_id_updated_at", + "unique": false, + "columnNames": [ + "incident_id", + "updated_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_incident_id_updated_at` ON `${TABLE_NAME}` (`incident_id` DESC, `updated_at` DESC)" + }, + { + "name": "index_lists_updated_at", + "unique": false, + "columnNames": [ + "updated_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_updated_at` ON `${TABLE_NAME}` (`updated_at` DESC)" + }, + { + "name": "index_lists_model_updated_at", + "unique": false, + "columnNames": [ + "model", + "updated_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_model_updated_at` ON `${TABLE_NAME}` (`model` DESC, `updated_at` DESC)" + }, + { + "name": "index_lists_parent_list_order", + "unique": false, + "columnNames": [ + "parent", + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_parent_list_order` ON `${TABLE_NAME}` (`parent`, `list_order`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "teams_root", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `local_modified_at` INTEGER NOT NULL DEFAULT 0, `synced_at` INTEGER NOT NULL DEFAULT 0, `local_global_uuid` TEXT NOT NULL DEFAULT '', `is_local_modified` INTEGER NOT NULL DEFAULT 0, `sync_attempt` INTEGER NOT NULL DEFAULT 0, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localModifiedAt", + "columnName": "local_modified_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isLocalModified", + "columnName": "is_local_modified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncAttempt", + "columnName": "sync_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_teams_root_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_teams_root_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_teams_root_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_root_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_teams_root_is_local_modified_local_modified_at", + "unique": false, + "columnNames": [ + "is_local_modified", + "local_modified_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_root_is_local_modified_local_modified_at` ON `${TABLE_NAME}` (`is_local_modified` DESC, `local_modified_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, `color` TEXT NOT NULL, `case_count` INTEGER NOT NULL, `case_complete_count` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `teams_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseCount", + "columnName": "case_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completeCount", + "columnName": "case_complete_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_teams_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_teams_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_teams_incident_id_name", + "unique": false, + "columnNames": [ + "incident_id", + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_incident_id_name` ON `${TABLE_NAME}` (`incident_id`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "teams_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "team_to_primary_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`team_id` INTEGER NOT NULL, `contact_id` INTEGER NOT NULL, PRIMARY KEY(`team_id`, `contact_id`), FOREIGN KEY(`team_id`) REFERENCES `teams`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contact_id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "teamId", + "columnName": "team_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contact_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "team_id", + "contact_id" + ] + }, + "indices": [ + { + "name": "idx_contact_to_team", + "unique": false, + "columnNames": [ + "contact_id", + "team_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_contact_to_team` ON `${TABLE_NAME}` (`contact_id`, `team_id`)" + } + ], + "foreignKeys": [ + { + "table": "teams", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "team_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contact_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_data_sync_parameters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `updated_before` INTEGER NOT NULL, `updated_after` INTEGER NOT NULL, `full_updated_before` INTEGER NOT NULL, `full_updated_after` INTEGER NOT NULL, `bounded_region` TEXT NOT NULL, `bounded_synced_at` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedBefore", + "columnName": "updated_before", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAfter", + "columnName": "updated_after", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalUpdatedBefore", + "columnName": "full_updated_before", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalUpdatedAfter", + "columnName": "full_updated_after", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "boundedRegion", + "columnName": "bounded_region", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "boundedSyncedAt", + "columnName": "bounded_synced_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a81b1821c83674f05be426c781fa7805')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteChangeDaoTest.kt b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteChangeDaoTest.kt index 02f8b50c1..076d00bcb 100644 --- a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteChangeDaoTest.kt +++ b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteChangeDaoTest.kt @@ -113,7 +113,9 @@ class WorksiteChangeDaoTest { testWorksiteNote(64, createdAtB, "note-b"), ), phone1 = "phone1", + phone1Notes = "phone1-notes", phone2 = "phone2", + phone2Notes = "phone2-notes", plusCode = "plus-code", postalCode = "postal-code", reportedBy = 573, @@ -210,6 +212,7 @@ class WorksiteChangeDaoTest { testWorksiteNote(41, createdAtB, "note-e"), ), phone1 = "phone1-change", + phone1Notes = "phone1-notes-change", phone2 = "", postalCode = "postal-code-change", state = "state-change", diff --git a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt index 22d368b4c..fa1d8201b 100644 --- a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt +++ b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt @@ -250,7 +250,9 @@ class WorksiteDaoTest { assertNotNull(full.autoContactFrequencyT) assertNotNull(full.email) assertNotNull(full.phone1) + assertNotNull(full.phone1Notes) assertNotNull(full.phone2) + assertNotNull(full.phone2Notes) assertNotNull(full.plusCode) assertNotNull(full.reportedBy) assertNotNull(full.what3Words) @@ -392,7 +394,9 @@ fun testWorksiteEntity( longitude = 0.0, name = "", phone1 = "", + phone1Notes = null, phone2 = null, + phone2Notes = null, phoneSearch = null, plusCode = null, postalCode = "", @@ -430,7 +434,9 @@ fun testWorksiteFullEntity( longitude = -534.15, name = "full worksite", phone1 = "345-414-7825", + phone1Notes = "phone-notes", phone2 = "835-621-8938", + phone2Notes = "phone2-notes", phoneSearch = null, plusCode = "code 123", postalCode = "83425", @@ -468,7 +474,9 @@ fun testWorksiteShortEntity( longitude = -157.15, name = "short worksite", phone1 = null, + phone1Notes = null, phone2 = null, + phone2Notes = null, phoneSearch = null, plusCode = null, postalCode = "83425-shrt", diff --git a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteSyncFillTest.kt b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteSyncFillTest.kt index 911176815..42f7c4846 100644 --- a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteSyncFillTest.kt +++ b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteSyncFillTest.kt @@ -144,7 +144,7 @@ class WorksiteSyncFillTest { reportedBy = 7835, state = "${coreA.state}-update", svi = coreA.svi!! * 2, - what3Words = "${coreA.what3Words}-update", + what3Words = coreA.what3Words, updatedAt = coreA.updatedAt.plus(99.seconds), ) val entitiesA = WorksiteEntities( diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt b/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt index 36093f0ef..c2afba1d3 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt @@ -119,7 +119,7 @@ import com.crisiscleanup.core.database.util.InstantConverter TeamMemberCrossRef::class, IncidentDataSyncParametersEntity::class, ], - version = 46, + version = 47, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3, spec = Schema2To3::class), @@ -166,6 +166,7 @@ import com.crisiscleanup.core.database.util.InstantConverter AutoMigration(from = 43, to = 44), AutoMigration(from = 44, to = 45), AutoMigration(from = 45, to = 46, spec = Schema45To46::class), + AutoMigration(from = 46, to = 47), ], exportSchema = true, ) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/LocalImageDaoPlus.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/LocalImageDaoPlus.kt index 521ce3c9d..c71f520bb 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/LocalImageDaoPlus.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/LocalImageDaoPlus.kt @@ -7,6 +7,7 @@ import com.crisiscleanup.core.database.model.NetworkFileLocalImageEntity import com.crisiscleanup.core.database.model.PopulatedLocalImageDescription import com.crisiscleanup.core.database.model.WorksiteLocalImageEntity import com.crisiscleanup.core.database.model.WorksiteNetworkFileCrossRef +import com.crisiscleanup.core.model.data.NetworkWorksiteFileIds import com.crisiscleanup.core.model.data.PhotoChangeDataProvider import javax.inject.Inject @@ -60,10 +61,12 @@ class LocalImageDaoPlus @Inject constructor( // PhotoChangeDataProvider - override suspend fun getDeletedPhotoNetworkFileIds(worksiteId: Long): Pair> = - db.withTransaction { - val networkWorksiteId = db.worksiteDao().getWorksiteNetworkId(worksiteId) - val deletedFileIds = db.networkFileDao().getDeletedPhotoNetworkFileIds(worksiteId) - Pair(networkWorksiteId, deletedFileIds) - } + override suspend fun getDeletedPhotoNetworkFileIds(worksiteId: Long) = db.withTransaction { + val networkWorksiteId = db.worksiteDao().getWorksiteNetworkId(worksiteId) + val deletedFileIds = db.networkFileDao().getDeletedPhotoNetworkFileIds(worksiteId) + NetworkWorksiteFileIds( + worksiteId = networkWorksiteId, + fileIds = deletedFileIds, + ) + } } diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/NetworkFileDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/NetworkFileDao.kt index b8c753a7b..85dc79da7 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/NetworkFileDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/NetworkFileDao.kt @@ -28,16 +28,20 @@ interface NetworkFileDao { ) """, ) - fun deleteDeleted(worksiteId: Long, keepIds: Collection) + fun deleteUnspecified(worksiteId: Long, keepIds: Collection) @Transaction @Query( """ DELETE FROM worksite_to_network_file - WHERE worksite_id=:worksiteId AND network_file_id NOT IN(:networkFileIds) + WHERE worksite_id=:worksiteId AND network_file_id NOT IN(:ids) """, ) - fun deleteUnspecifiedCrossReferences(worksiteId: Long, networkFileIds: Collection) + fun deleteUnspecifiedCrossReferences(worksiteId: Long, ids: Collection) + + @Transaction + @Query("DELETE FROM worksite_to_network_file WHERE worksite_id=:worksiteId") + fun deleteWorksiteNetworkFiles(worksiteId: Long) @Insert(onConflict = OnConflictStrategy.IGNORE) fun insertIgnoreCrossReferences(crossReferences: Collection) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt index b042b0701..2899131fa 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt @@ -237,7 +237,9 @@ interface WorksiteDao { longitude =:longitude, name =:name, phone1 =COALESCE(:phone1, phone1), + phone1_notes =COALESCE(:phone1Notes, phone1_notes), phone2 =COALESCE(:phone2, phone2), + phone2_notes =COALESCE(:phone2Notes, phone2_notes), phone_search =COALESCE(:phoneSearch, phone_search), plus_code =COALESCE(:plusCode, plus_code), postal_code =:postalCode, @@ -270,7 +272,9 @@ interface WorksiteDao { longitude: Double, name: String, phone1: String?, + phone1Notes: String?, phone2: String?, + phone2Notes: String?, phoneSearch: String?, plusCode: String?, postalCode: String, @@ -293,11 +297,13 @@ interface WorksiteDao { email =COALESCE(email, :email), favorite_id =COALESCE(favorite_id, :favoriteId), phone1 =CASE WHEN LENGTH(COALESCE(phone1,''))<2 THEN :phone1 ELSE phone1 END, + phone1_notes=COALESCE(phone1_notes, :phone1Notes), phone2 =COALESCE(phone2, :phone2), + phone2_notes=COALESCE(phone2_notes, :phone2Notes), plus_code =COALESCE(plus_code, :plusCode), svi =COALESCE(svi, :svi), - reported_by =COALESCE(reported_by, :reportedBy), - what3Words =COALESCE(what3Words, :what3Words), + reported_by =COALESCE(:reportedBy, reported_by), + what3Words =COALESCE(:what3Words, what3Words), network_photo_count =COALESCE(:photoCount, network_photo_count) WHERE id=:id """, @@ -310,7 +316,9 @@ interface WorksiteDao { email: String?, favoriteId: Long?, phone1: String?, + phone1Notes: String?, phone2: String?, + phone2Notes: String?, plusCode: String?, svi: Float?, reportedBy: Long?, diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt index 776ee6a22..a70d25186 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt @@ -115,14 +115,18 @@ class WorksiteDaoPlus @Inject constructor( worksiteId: Long, files: List, ) = db.withTransaction { + val networkFileDao = db.networkFileDao() + if (files.isEmpty()) { + networkFileDao.deleteWorksiteNetworkFiles(worksiteId) return@withTransaction } - val networkFileDao = db.networkFileDao() networkFileDao.upsert(files) + // TODO Update corresponding network_file_local_images.is_deleted as necessary + val ids = files.map(NetworkFileEntity::id).toSet() - networkFileDao.deleteDeleted(worksiteId, ids) + networkFileDao.deleteUnspecified(worksiteId, ids) networkFileDao.deleteUnspecifiedCrossReferences(worksiteId, ids) val networkFileCrossReferences = ids.map { WorksiteNetworkFileCrossRef(worksiteId, it) } networkFileDao.insertIgnoreCrossReferences(networkFileCrossReferences) @@ -195,7 +199,9 @@ class WorksiteDaoPlus @Inject constructor( longitude = longitude, name = name, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, phoneSearch = phoneSearch, plusCode = plusCode, postalCode = postalCode, @@ -373,7 +379,9 @@ class WorksiteDaoPlus @Inject constructor( email = email, favoriteId = favoriteId, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, plusCode = plusCode, svi = svi, reportedBy = reportedBy, diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/NetworkFileEntity.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/NetworkFileEntity.kt index d3dc42584..96cf6e43d 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/NetworkFileEntity.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/NetworkFileEntity.kt @@ -57,6 +57,7 @@ data class NetworkFileEntity( data class WorksiteNetworkFileCrossRef( @ColumnInfo("worksite_id") val worksiteId: Long, + // NetworkFile.id not NetworkFile.fileId @ColumnInfo("network_file_id") val networkFileId: Long, ) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedFileIds.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedFileIds.kt new file mode 100644 index 000000000..d31534016 --- /dev/null +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedFileIds.kt @@ -0,0 +1,6 @@ +package com.crisiscleanup.core.database.model + +data class PopulatedFileIds( + val fileId: Long, + val networkFileId: Long, +) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedLocalWorksite.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedLocalWorksite.kt index 890dfe24d..296e0442a 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedLocalWorksite.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedLocalWorksite.kt @@ -129,7 +129,9 @@ fun PopulatedLocalWorksite.asExternalModel( .map(WorksiteNoteEntity::asExternalModel), networkId = networkId, phone1 = phone1 ?: "", + phone1Notes = phone1Notes ?: "", phone2 = phone2 ?: "", + phone2Notes = phone2Notes ?: "", plusCode = plusCode, postalCode = postalCode, reportedBy = reportedBy, diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt index 872849abd..b635248bd 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt @@ -59,7 +59,9 @@ fun PopulatedWorksite.asExternalModel(): Worksite { longitude = longitude, name = name, phone1 = phone1 ?: "", + phone1Notes = phone1Notes ?: "", phone2 = phone2 ?: "", + phone2Notes = phone2Notes ?: "", plusCode = plusCode, postalCode = postalCode, reportedBy = reportedBy, @@ -141,6 +143,7 @@ data class PopulatedWorksiteMapVisual( private val highPriorityFlagLiteral = WorksiteFlagType.HighPriority.literal private val duplicateFlagLiteral = WorksiteFlagType.Duplicate.literal +private val markedForDeleteFlagLiteral = WorksiteFlagType.MarkForDeletion.literal fun PopulatedWorksiteMapVisual.asExternalModel(isFilteredOut: Boolean = false) = WorksiteMapMark( id = id, latitude = latitude, @@ -155,6 +158,7 @@ fun PopulatedWorksiteMapVisual.asExternalModel(isFilteredOut: Boolean = false) = it.reasonT == highPriorityFlagLiteral }, isDuplicate = flags.any { it.reasonT == duplicateFlagLiteral }, + isMarkedForDelete = flags.any { it.reasonT == markedForDeleteFlagLiteral }, isFilteredOut = isFilteredOut, hasPhotos = networkPhotoCount > 0 || fileImages.any { !it.isDeleted } || diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt index 26eef2661..2a70e2889 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt @@ -35,7 +35,9 @@ fun Worksite.asEntities( longitude = longitude, name = name, phone1 = phone1, + phone1Notes = phone1Notes, phone2 = phone2, + phone2Notes = phone2Notes, phoneSearch = searchablePhoneNumbers(phone1, phone2), plusCode = plusCode, postalCode = postalCode, diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt index 4365c82c3..491b29bb4 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt @@ -117,8 +117,12 @@ data class WorksiteEntity( val longitude: Double, val name: String, val phone1: String?, + @ColumnInfo("phone1_notes", defaultValue = "") + val phone1Notes: String?, @ColumnInfo(defaultValue = "") val phone2: String?, + @ColumnInfo("phone2_notes", defaultValue = "") + val phone2Notes: String?, @ColumnInfo("phone_search", defaultValue = "") val phoneSearch: String?, @ColumnInfo("plus_code", defaultValue = "") diff --git a/core/datastore-proto/src/main/proto/com/crisiscleanup/core/data/app_metrics.proto b/core/datastore-proto/src/main/proto/com/crisiscleanup/core/data/app_metrics.proto index 8c167d2d9..13b690c9d 100644 --- a/core/datastore-proto/src/main/proto/com/crisiscleanup/core/data/app_metrics.proto +++ b/core/datastore-proto/src/main/proto/com/crisiscleanup/core/data/app_metrics.proto @@ -17,4 +17,5 @@ message AppMetrics { AppMinUseProto minBuildSupport = 5; int64 appInstallVersion = 6; + int64 appPublishedVersion = 7; } diff --git a/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppMetricsDataSource.kt b/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppMetricsDataSource.kt index b0eea38e8..54491ce3b 100644 --- a/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppMetricsDataSource.kt +++ b/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppMetricsDataSource.kt @@ -43,6 +43,7 @@ class LocalAppMetricsDataSource @Inject constructor( ), appInstallVersion = it.appInstallVersion, + appPublishedVersion = it.appPublishedVersion, ) } @@ -73,7 +74,10 @@ class LocalAppMetricsDataSource @Inject constructor( } } - suspend fun setMinSupportedAppVersion(supportedAppVersion: MinSupportedAppVersion) { + suspend fun setAppVersions( + supportedAppVersion: MinSupportedAppVersion, + publishedVersion: Long, + ) { val builder = AppMinUseProto.newBuilder() builder.minVersion = supportedAppVersion.minBuild builder.title = supportedAppVersion.title @@ -82,6 +86,7 @@ class LocalAppMetricsDataSource @Inject constructor( appMetrics.updateData { it.copy { minBuildSupport = builder.build() + appPublishedVersion = publishedVersion } } } diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt index c59da0d22..39f75313a 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt @@ -14,11 +14,13 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.CloudSync import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Directions import androidx.compose.material.icons.filled.Domain +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore @@ -61,6 +63,7 @@ private val icons = Icons.Default object CrisisCleanupIcons { val Account = icons.PersonOutline val Add = icons.Add + val AppUpdateAvailable = icons.Download val ArrowBack = Icons.AutoMirrored.Filled.ArrowBackIos val ArrowBack2 = Icons.AutoMirrored.Filled.ArrowBack val ArrowDropDown = icons.ArrowDropDown @@ -72,6 +75,7 @@ object CrisisCleanupIcons { val Clear = icons.Clear val CloudSync = icons.CloudSync val Cloud = icons.Cloud + val CloudOff = icons.CloudOff val Close = icons.Close val Dashboard = R.drawable.ic_dashboard val Delete = icons.Delete diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt index 5130c4b41..9c4bb895e 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt @@ -58,8 +58,9 @@ class InMemoryDotProvider @Inject constructor( ): BitmapDescriptor? { val colors = getMapMarkerColors( cacheKey.statusClaim, - cacheKey.isDuplicate, - cacheKey.isFilteredOut, + isDuplicate = cacheKey.isDuplicate, + isMarkedForDelete = cacheKey.isMarkedForDelete, + isFilteredOut = cacheKey.isFilteredOut, isVisited = false, isDot = true, ) @@ -83,11 +84,17 @@ class InMemoryDotProvider @Inject constructor( isImportant: Boolean, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean, + isMarkedForDelete: Boolean, isFilteredOut: Boolean, isVisited: Boolean, hasPhotos: Boolean, ): BitmapDescriptor? { - val cacheKey = DotCacheKey(statusClaim, isDuplicate, isFilteredOut) + val cacheKey = DotCacheKey( + statusClaim, + isDuplicate = isDuplicate, + isMarkedForDelete = isMarkedForDelete, + isFilteredOut = isFilteredOut, + ) synchronized(cache) { cache[cacheKey]?.let { return it @@ -102,11 +109,17 @@ class InMemoryDotProvider @Inject constructor( workType: WorkTypeType, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean, + isMarkedForDelete: Boolean, isFilteredOut: Boolean, isVisited: Boolean, hasPhotos: Boolean, ): Bitmap? { - val cacheKey = DotCacheKey(statusClaim, isDuplicate, isFilteredOut) + val cacheKey = DotCacheKey( + statusClaim, + isDuplicate = isDuplicate, + isMarkedForDelete = isMarkedForDelete, + isFilteredOut = isFilteredOut, + ) synchronized(cache) { bitmapCache.get(cacheKey)?.let { return it @@ -176,5 +189,6 @@ data class DotDrawProperties( private data class DotCacheKey( val statusClaim: WorkTypeStatusClaim, val isDuplicate: Boolean = false, + val isMarkedForDelete: Boolean = false, val isFilteredOut: Boolean = false, ) diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt index 6732b7628..d569776cd 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt @@ -19,6 +19,7 @@ interface MapCaseIconProvider { isImportant: Boolean, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean = false, + isMarkedForDelete: Boolean = false, isFilteredOut: Boolean = false, isVisited: Boolean = false, hasPhotos: Boolean = false, @@ -29,6 +30,7 @@ interface MapCaseIconProvider { workType: WorkTypeType, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean = false, + isMarkedForDelete: Boolean = false, isFilteredOut: Boolean = false, isVisited: Boolean = false, hasPhotos: Boolean = false, diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapMarkerIconProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapMarkerIconProvider.kt index 53154e4c1..aac198372 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapMarkerIconProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapMarkerIconProvider.kt @@ -3,6 +3,7 @@ package com.crisiscleanup.core.mapmarker import android.graphics.Bitmap import android.graphics.Canvas import androidx.annotation.DrawableRes +import androidx.core.graphics.createBitmap import com.crisiscleanup.core.common.AndroidResourceProvider import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory @@ -57,11 +58,7 @@ class CrisisCleanupDrawableResourceBitmapProvider @Inject constructor( val heightPx = resourceProvider.dpToPx(height).toInt() val drawable = resourceProvider.getDrawable(drawableResId) - val output = Bitmap.createBitmap( - widthPx, - heightPx, - Bitmap.Config.ARGB_8888, - ) + val output = createBitmap(widthPx, heightPx) val canvas = Canvas(output) drawable.setBounds(0, 0, canvas.width, canvas.height) diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MarkerColor.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MarkerColor.kt index cc64102ce..eb55be28b 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MarkerColor.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MarkerColor.kt @@ -75,6 +75,7 @@ private const val DUPLICATE_MARKER_ALPHA = 0.3f internal fun getMapMarkerColors( statusClaim: WorkTypeStatusClaim, isDuplicate: Boolean, + isMarkedForDelete: Boolean, isFilteredOut: Boolean, isVisited: Boolean, isDot: Boolean, @@ -85,7 +86,7 @@ internal fun getMapMarkerColors( colors = statusMapMarkerColors[status] ?: statusMapMarkerColors[Unknown]!! } - if (isDuplicate) { + if (isDuplicate || isMarkedForDelete) { colors = colors.copy( fill = colors.fill.copy(alpha = DUPLICATE_MARKER_ALPHA), stroke = colors.stroke.copy(alpha = DUPLICATE_MARKER_ALPHA), diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt index a5c964e49..8a9af5b04 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt @@ -137,6 +137,7 @@ class WorkTypeIconProvider @Inject constructor( isImportant: Boolean, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean, + isMarkedForDelete: Boolean, isFilteredOut: Boolean, isVisited: Boolean, hasPhotos: Boolean, @@ -148,12 +149,13 @@ class WorkTypeIconProvider @Inject constructor( isFavorite = isFavorite, isImportant = isImportant, isDuplicate = isDuplicate, + isMarkedForDelete = isMarkedForDelete, isFilteredOut = isFilteredOut, isVisited = isVisited, hasPhotos = hasPhotos, ) synchronized(cache) { - cache.get(cacheKey)?.let { + cache[cacheKey]?.let { return it } } @@ -166,6 +168,7 @@ class WorkTypeIconProvider @Inject constructor( workType: WorkTypeType, hasMultipleWorkTypes: Boolean, isDuplicate: Boolean, + isMarkedForDelete: Boolean, isFilteredOut: Boolean, isVisited: Boolean, hasPhotos: Boolean, @@ -173,21 +176,22 @@ class WorkTypeIconProvider @Inject constructor( val cacheKey = WorkTypeIconCacheKey( statusClaim, workType, - hasMultipleWorkTypes, - isDuplicate, - isFilteredOut, - isVisited, - hasPhotos, + hasMultipleWorkTypes = hasMultipleWorkTypes, + isDuplicate = isDuplicate, + isMarkedForDelete = isMarkedForDelete, + isFilteredOut = isFilteredOut, + isVisited = isVisited, + hasPhotos = hasPhotos, ) synchronized(cache) { - bitmapCache.get(cacheKey)?.let { + bitmapCache[cacheKey]?.let { return it } } cacheIconBitmap(cacheKey) synchronized(cache) { - bitmapCache.get(cacheKey)?.let { + bitmapCache[cacheKey]?.let { return it } return null @@ -220,9 +224,10 @@ class WorkTypeIconProvider @Inject constructor( val colors = getMapMarkerColors( cacheKey.statusClaim, - cacheKey.isDuplicate, - cacheKey.isFilteredOut, - cacheKey.isVisited, + isDuplicate = cacheKey.isDuplicate, + isMarkedForDelete = cacheKey.isMarkedForDelete, + isFilteredOut = cacheKey.isFilteredOut, + isVisited = cacheKey.isVisited, isDot = false, ) val fillAlpha = if (colors.fill.alpha < 1) (colors.fill.alpha * 255).toInt() else 255 @@ -374,6 +379,7 @@ private data class WorkTypeIconCacheKey( val isFavorite: Boolean = false, val isImportant: Boolean = false, val isDuplicate: Boolean = false, + val isMarkedForDelete: Boolean = false, val isFilteredOut: Boolean = false, val isVisited: Boolean = false, val hasPhotos: Boolean = false, diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/AppMetricsData.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/AppMetricsData.kt index 0750bbb11..85ad84e87 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/AppMetricsData.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/AppMetricsData.kt @@ -14,6 +14,7 @@ data class AppMetricsData( val minSupportedAppVersion: MinSupportedAppVersion, val appInstallVersion: Long, + val appPublishedVersion: Long, ) data class MinSupportedAppVersion( diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/PhotoChangeDataProvider.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/PhotoChangeDataProvider.kt index bdc835c27..822ed7378 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/PhotoChangeDataProvider.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/PhotoChangeDataProvider.kt @@ -1,5 +1,10 @@ package com.crisiscleanup.core.model.data interface PhotoChangeDataProvider { - suspend fun getDeletedPhotoNetworkFileIds(worksiteId: Long): Pair> + suspend fun getDeletedPhotoNetworkFileIds(worksiteId: Long): NetworkWorksiteFileIds } + +data class NetworkWorksiteFileIds( + val worksiteId: Long, + val fileIds: List, +) diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/Worksite.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/Worksite.kt index fca9a1946..41899a052 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/Worksite.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/Worksite.kt @@ -35,7 +35,9 @@ data class Worksite( val networkId: Long, val notes: List = emptyList(), val phone1: String, + val phone1Notes: String = "", val phone2: String, + val phone2Notes: String = "", val plusCode: String? = null, val postalCode: String, val reportedBy: Long?, diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt index 6a4b014b9..23623c9ff 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt @@ -10,6 +10,7 @@ data class WorksiteMapMark( val isFavorite: Boolean = false, val isHighPriority: Boolean = false, val isDuplicate: Boolean = false, + val isMarkedForDelete: Boolean = false, /** * Is this mark excluded by filters * diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupAuthApi.kt b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupAuthApi.kt index 1628e0480..968f7c396 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupAuthApi.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupAuthApi.kt @@ -1,13 +1,10 @@ package com.crisiscleanup.core.network -import com.crisiscleanup.core.network.model.NetworkAuthResult import com.crisiscleanup.core.network.model.NetworkCodeAuthResult import com.crisiscleanup.core.network.model.NetworkOauthResult import com.crisiscleanup.core.network.model.NetworkPhoneOneTimePasswordResult interface CrisisCleanupAuthApi { - suspend fun login(email: String, password: String): NetworkAuthResult - suspend fun oauthLogin(email: String, password: String): NetworkOauthResult suspend fun magicLinkLogin(token: String): NetworkCodeAuthResult diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt index 8c0322125..446aec72a 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt @@ -28,7 +28,7 @@ import com.crisiscleanup.core.network.model.NetworkWorksitesPageResult import kotlinx.datetime.Instant interface CrisisCleanupNetworkDataSource { - suspend fun getProfileData(): NetworkAccountProfileResult + suspend fun getProfileData(accountId: Long): NetworkAccountProfileResult suspend fun getOrganizations(organizations: List): List diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/appsupport/NetworkAppSupportInfo.kt b/core/network/src/main/java/com/crisiscleanup/core/network/appsupport/NetworkAppSupportInfo.kt index b74bf52cd..efa253a35 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/appsupport/NetworkAppSupportInfo.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/appsupport/NetworkAppSupportInfo.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class NetworkAppSupportInfo( + val publishedVersion: Long?, val minBuildVersion: Long, val title: String?, val message: String, diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/fake/FakeAuthApi.kt b/core/network/src/main/java/com/crisiscleanup/core/network/fake/FakeAuthApi.kt deleted file mode 100644 index 0b8c3cbea..000000000 --- a/core/network/src/main/java/com/crisiscleanup/core/network/fake/FakeAuthApi.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.crisiscleanup.core.network.fake - -import com.crisiscleanup.core.network.CrisisCleanupAuthApi -import com.crisiscleanup.core.network.model.NetworkAuthResult -import com.crisiscleanup.core.network.model.NetworkAuthUserClaims -import com.crisiscleanup.core.network.model.NetworkCodeAuthResult -import com.crisiscleanup.core.network.model.NetworkOauthResult -import com.crisiscleanup.core.network.model.NetworkPhoneOneTimePasswordResult -import kotlinx.coroutines.delay -import kotlinx.datetime.Clock -import javax.inject.Inject - -class FakeAuthApi @Inject constructor() : CrisisCleanupAuthApi { - override suspend fun login(email: String, password: String): NetworkAuthResult { - delay(1000) - return NetworkAuthResult( - accessToken = "access-token", - claims = NetworkAuthUserClaims( - id = 1, - email = "demo@crisiscleanup.org", - mobile = "1234567890", - firstName = "Demo", - lastName = "User", - files = emptyList(), - hasAcceptedTerms = true, - acceptedTermsTimestamp = Clock.System.now(), - approvedIncidents = setOf(1), - activeRoles = setOf(53), - ), - ) - } - - override suspend fun logout() { - delay(500) - } - - override suspend fun oauthLogin(email: String, password: String): NetworkOauthResult { - delay(1000) - return NetworkOauthResult( - refreshToken = "refresh-token", - accessToken = "access-token", - expiresIn = 3600, - ) - } - - override suspend fun magicLinkLogin(token: String) = NetworkCodeAuthResult( - refreshToken = "refresh", - accessToken = "access", - expiresIn = 3600, - ) - - override suspend fun verifyPhoneCode( - phoneNumber: String, - code: String, - ) = NetworkPhoneOneTimePasswordResult() - - override suspend fun oneTimePasswordLogin( - accountId: Long, - oneTimePasswordId: Long, - ) = NetworkCodeAuthResult( - refreshToken = "refresh", - accessToken = "access", - expiresIn = 3600, - ) - - private var refreshTokenCounter = 1 - override suspend fun refreshTokens(refreshToken: String): NetworkOauthResult { - delay(1000) - return NetworkOauthResult( - refreshToken = "refresh-token-${refreshTokenCounter++}", - accessToken = "access-token", - expiresIn = 3600, - ) - } -} diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAccountProfileResult.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAccountProfileResult.kt index adf5cb819..5578e3871 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAccountProfileResult.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAccountProfileResult.kt @@ -14,7 +14,7 @@ data class NetworkAccountProfileResult( @SerialName("accepted_terms_timestamp") val acceptedTermsTimestamp: Instant?, val files: List?, - val organization: NetworkOrganizationShort, + val organization: NetworkOrganizationShort?, @SerialName("active_roles") - val activeRoles: Set, + val activeRoles: Set?, ) diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAuth.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAuth.kt index cf2f416e8..48de01dfb 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAuth.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkAuth.kt @@ -1,54 +1,8 @@ package com.crisiscleanup.core.network.model -import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -@Serializable -data class NetworkAuthPayload( - val email: String, - val password: String, -) - -@Serializable -data class NetworkAuthResult( - val errors: List? = null, - @SerialName("access_token") - val accessToken: String? = null, - @SerialName("user_claims") - val claims: NetworkAuthUserClaims? = null, - val organizations: NetworkAuthOrganization? = null, -) - -@Serializable -data class NetworkAuthUserClaims( - // UPDATE NetworkAuthTest in conjunction with changes here - val id: Long, - val email: String, - val mobile: String, - @SerialName("first_name") - val firstName: String, - @SerialName("last_name") - val lastName: String, - @SerialName("approved_incidents") - val approvedIncidents: Set, - @SerialName("accepted_terms") - val hasAcceptedTerms: Boolean?, - @SerialName("accepted_terms_timestamp") - val acceptedTermsTimestamp: Instant?, - val files: List?, - @SerialName("active_roles") - val activeRoles: Set, -) - -@Serializable -data class NetworkAuthOrganization( - val id: Long, - val name: String, - @SerialName("is_active") - val isActive: Boolean, -) - @Serializable data class NetworkOauthPayload( val username: String, diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkUser.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkUser.kt index 510412f71..4ae8573b6 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkUser.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkUser.kt @@ -15,6 +15,7 @@ data class NetworkUser( val files: List, ) +// UPDATE NetworkAccountTest in conjunction with changes here @Serializable data class NetworkUserProfile( val id: Long, diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt index c6e1381dd..76c9038b6 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt @@ -28,7 +28,7 @@ data class NetworkWorksiteFull( val email: String? = null, val events: List, val favorite: NetworkType?, - val files: List, + val files: List? = emptyList(), val flags: List, @SerialName("form_data") val formData: List, @@ -39,7 +39,11 @@ data class NetworkWorksiteFull( val name: String, val notes: List, val phone1: String, + @SerialName("phone1_notes") + val phone1Notes: String? = null, val phone2: String?, + @SerialName("phone2_notes") + val phone2Notes: String? = null, @SerialName("pluscode") val plusCode: String? = null, @SerialName("postal_code") @@ -48,8 +52,6 @@ data class NetworkWorksiteFull( val reportedBy: Long?, val state: String, val svi: Float?, -// @SerialName("time") -// val times: List