From 1000f2411cf64391cc02dbc57c81f49cb8f57714 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sun, 8 Feb 2026 19:21:07 +0530 Subject: [PATCH] add room db --- AGENTS.md | 8 ++ app/build.gradle.kts | 22 ++- .../local/booking/BookingDetailsCacheDao.kt | 30 ++++ .../booking/BookingDetailsCacheEntity.kt | 136 ++++++++++++++++++ .../local/booking/BookingDetailsRepository.kt | 58 ++++++++ .../data/local/core/AppDatabase.kt | 67 +++++++++ .../data/local/core/LocalDatabaseProvider.kt | 24 ++++ .../data/local/core/RoomConverters.kt | 29 ++++ .../local/roomstay/ActiveRoomStayCacheDao.kt | 61 ++++++++ .../roomstay/ActiveRoomStayCacheEntity.kt | 71 +++++++++ .../roomstay/ActiveRoomStayRepository.kt | 48 +++++++ .../ui/roomstay/ActiveRoomStaysViewModel.kt | 94 +++++++++--- .../ui/roomstay/BookingDetailsTabsScreen.kt | 66 ++++----- .../ui/roomstay/BookingDetailsViewModel.kt | 117 ++++++++++++--- .../ui/roomstay/BookingRoomStaysViewModel.kt | 108 +++++++++----- build.gradle.kts | 4 +- gradle.properties | 2 + gradle/libs.versions.toml | 7 + 18 files changed, 823 insertions(+), 129 deletions(-) create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheDao.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheEntity.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsRepository.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/core/AppDatabase.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/core/LocalDatabaseProvider.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/core/RoomConverters.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheDao.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheEntity.kt create mode 100644 app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayRepository.kt diff --git a/AGENTS.md b/AGENTS.md index 01f6b62..c900ac7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -576,6 +576,14 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance - Imports/packages follow the structure above. - Build passes: `./gradlew :app:compileDebugKotlin`. +### Room DB synchronization rule (mandatory) + +- For any editable API-backed field, follow this write path only: `UI action -> server request -> Room DB update -> UI reacts from Room observers`. +- Server is source of truth; do not bypass server by writing final business state directly from UI. +- UI must render from Room-backed state, not from one-off API responses or direct text mutation. +- Avoid forced full refresh after each mutation unless strictly required by backend limitations; prefer targeted Room updates for linked entities. +- On mutation failure, keep prior DB state unchanged and surface error state to UI. + ### Guest Documents Authorization (mandatory) - View access: `ADMIN`, `MANAGER` (and super admin). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 728fe2d..72cc98f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,14 +1,17 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) id("com.google.gms.google-services") } -android { +extensions.configure("android") { namespace = "com.android.trisolarispms" - compileSdk { - version = release(36) - } + compileSdk = 36 defaultConfig { applicationId = "com.android.trisolarispms" @@ -29,16 +32,24 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + buildFeatures { compose = true buildConfig = true } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -63,6 +74,9 @@ dependencies { implementation(libs.calendar.compose) implementation(libs.libphonenumber) implementation(libs.zxing.core) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth.ktx) implementation(libs.kotlinx.coroutines.play.services) diff --git a/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheDao.kt b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheDao.kt new file mode 100644 index 0000000..ad0bbaf --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheDao.kt @@ -0,0 +1,30 @@ +package com.android.trisolarispms.data.local.booking + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface BookingDetailsCacheDao { + @Query( + """ + SELECT * FROM booking_details_cache + WHERE propertyId = :propertyId AND bookingId = :bookingId + LIMIT 1 + """ + ) + fun observe(propertyId: String, bookingId: String): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(entity: BookingDetailsCacheEntity) + + @Query( + """ + DELETE FROM booking_details_cache + WHERE propertyId = :propertyId AND bookingId = :bookingId + """ + ) + suspend fun delete(propertyId: String, bookingId: String) +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheEntity.kt b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheEntity.kt new file mode 100644 index 0000000..0bf6361 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsCacheEntity.kt @@ -0,0 +1,136 @@ +package com.android.trisolarispms.data.local.booking + +import androidx.room.Entity +import com.android.trisolarispms.data.api.model.BookingDetailsResponse + +@Entity( + tableName = "booking_details_cache", + primaryKeys = ["propertyId", "bookingId"] +) +data class BookingDetailsCacheEntity( + val propertyId: String, + val bookingId: String, + val detailsId: String? = null, + val status: String? = null, + val guestId: String? = null, + val guestName: String? = null, + val guestPhone: String? = null, + val guestNationality: String? = null, + val guestAge: String? = null, + val guestAddressText: String? = null, + val guestSignatureUrl: String? = null, + val vehicleNumbers: List = emptyList(), + val roomNumbers: List = emptyList(), + val source: String? = null, + val fromCity: String? = null, + val toCity: String? = null, + val memberRelation: String? = null, + val transportMode: String? = null, + val expectedCheckInAt: String? = null, + val expectedCheckOutAt: String? = null, + val checkInAt: String? = null, + val checkOutAt: String? = null, + val adultCount: Int? = null, + val maleCount: Int? = null, + val femaleCount: Int? = null, + val childCount: Int? = null, + val totalGuestCount: Int? = null, + val expectedGuestCount: Int? = null, + val totalNightlyRate: Long? = null, + val notes: String? = null, + val registeredByName: String? = null, + val registeredByPhone: String? = null, + val expectedPay: Long? = null, + val amountCollected: Long? = null, + val pending: Long? = null, + val billableNights: Long? = null, + val billingMode: String? = null, + val billingCheckinTime: String? = null, + val billingCheckoutTime: String? = null, + val updatedAtEpochMs: Long = System.currentTimeMillis() +) + +internal fun BookingDetailsResponse.toCacheEntity( + propertyId: String, + bookingId: String +): BookingDetailsCacheEntity = BookingDetailsCacheEntity( + propertyId = propertyId, + bookingId = bookingId, + detailsId = id, + status = status, + guestId = guestId, + guestName = guestName, + guestPhone = guestPhone, + guestNationality = guestNationality, + guestAge = guestAge, + guestAddressText = guestAddressText, + guestSignatureUrl = guestSignatureUrl, + vehicleNumbers = vehicleNumbers, + roomNumbers = roomNumbers, + source = source, + fromCity = fromCity, + toCity = toCity, + memberRelation = memberRelation, + transportMode = transportMode, + expectedCheckInAt = expectedCheckInAt, + expectedCheckOutAt = expectedCheckOutAt, + checkInAt = checkInAt, + checkOutAt = checkOutAt, + adultCount = adultCount, + maleCount = maleCount, + femaleCount = femaleCount, + childCount = childCount, + totalGuestCount = totalGuestCount, + expectedGuestCount = expectedGuestCount, + totalNightlyRate = totalNightlyRate, + notes = notes, + registeredByName = registeredByName, + registeredByPhone = registeredByPhone, + expectedPay = expectedPay, + amountCollected = amountCollected, + pending = pending, + billableNights = billableNights, + billingMode = billingMode, + billingCheckinTime = billingCheckinTime, + billingCheckoutTime = billingCheckoutTime +) + +internal fun BookingDetailsCacheEntity.toApiModel(): BookingDetailsResponse = BookingDetailsResponse( + id = detailsId ?: bookingId, + status = status, + guestId = guestId, + guestName = guestName, + guestPhone = guestPhone, + guestNationality = guestNationality, + guestAge = guestAge, + guestAddressText = guestAddressText, + guestSignatureUrl = guestSignatureUrl, + vehicleNumbers = vehicleNumbers, + roomNumbers = roomNumbers, + source = source, + fromCity = fromCity, + toCity = toCity, + memberRelation = memberRelation, + transportMode = transportMode, + expectedCheckInAt = expectedCheckInAt, + expectedCheckOutAt = expectedCheckOutAt, + checkInAt = checkInAt, + checkOutAt = checkOutAt, + adultCount = adultCount, + maleCount = maleCount, + femaleCount = femaleCount, + childCount = childCount, + totalGuestCount = totalGuestCount, + expectedGuestCount = expectedGuestCount, + totalNightlyRate = totalNightlyRate, + notes = notes, + registeredByName = registeredByName, + registeredByPhone = registeredByPhone, + expectedPay = expectedPay, + amountCollected = amountCollected, + pending = pending, + billableNights = billableNights, + billingMode = billingMode, + billingCheckinTime = billingCheckinTime, + billingCheckoutTime = billingCheckoutTime +) diff --git a/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsRepository.kt b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsRepository.kt new file mode 100644 index 0000000..846518d --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/booking/BookingDetailsRepository.kt @@ -0,0 +1,58 @@ +package com.android.trisolarispms.data.local.booking + +import com.android.trisolarispms.data.api.core.ApiClient +import com.android.trisolarispms.data.api.core.ApiService +import com.android.trisolarispms.data.api.model.BookingDetailsResponse +import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class BookingDetailsRepository( + private val dao: BookingDetailsCacheDao, + private val createApi: () -> ApiService = { ApiClient.create() } +) { + fun observeBookingDetails( + propertyId: String, + bookingId: String + ): Flow = + dao.observe(propertyId = propertyId, bookingId = bookingId).map { cached -> + cached?.toApiModel() + } + + suspend fun refreshBookingDetails( + propertyId: String, + bookingId: String + ): Result = runCatching { + val response = createApi().getBookingDetails(propertyId = propertyId, bookingId = bookingId) + if (!response.isSuccessful) { + throw IllegalStateException("Load failed: ${response.code()}") + } + val body = response.body() ?: throw IllegalStateException("Load failed: empty response") + dao.upsert(body.toCacheEntity(propertyId = propertyId, bookingId = bookingId)) + } + + suspend fun updateExpectedDates( + propertyId: String, + bookingId: String, + body: BookingExpectedDatesRequest + ): Result = runCatching { + val api = createApi() + val response = api.updateExpectedDates( + propertyId = propertyId, + bookingId = bookingId, + body = body + ) + if (!response.isSuccessful) { + throw IllegalStateException("Update failed: ${response.code()}") + } + refreshBookingDetails(propertyId = propertyId, bookingId = bookingId).getOrThrow() + } + + suspend fun storeSnapshot( + propertyId: String, + bookingId: String, + details: BookingDetailsResponse + ) { + dao.upsert(details.toCacheEntity(propertyId = propertyId, bookingId = bookingId)) + } +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/core/AppDatabase.kt b/app/src/main/java/com/android/trisolarispms/data/local/core/AppDatabase.kt new file mode 100644 index 0000000..53f9795 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/core/AppDatabase.kt @@ -0,0 +1,67 @@ +package com.android.trisolarispms.data.local.core + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import com.android.trisolarispms.data.local.booking.BookingDetailsCacheDao +import com.android.trisolarispms.data.local.booking.BookingDetailsCacheEntity +import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheDao +import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayCacheEntity +import androidx.room.migration.Migration + +@Database( + entities = [ + BookingDetailsCacheEntity::class, + ActiveRoomStayCacheEntity::class + ], + version = 2, + exportSchema = false +) +@TypeConverters(RoomConverters::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun bookingDetailsCacheDao(): BookingDetailsCacheDao + abstract fun activeRoomStayCacheDao(): ActiveRoomStayCacheDao + + companion object { + val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `active_room_stay_cache` ( + `roomStayId` TEXT NOT NULL, + `propertyId` TEXT NOT NULL, + `bookingId` TEXT, + `guestId` TEXT, + `guestName` TEXT, + `guestPhone` TEXT, + `roomId` TEXT, + `roomNumber` INTEGER, + `roomTypeCode` TEXT, + `roomTypeName` TEXT, + `fromAt` TEXT, + `checkinAt` TEXT, + `expectedCheckoutAt` TEXT, + `nightlyRate` INTEGER, + `currency` TEXT, + `updatedAtEpochMs` INTEGER NOT NULL, + PRIMARY KEY(`roomStayId`) + ) + """.trimIndent() + ) + db.execSQL( + """ + CREATE INDEX IF NOT EXISTS `index_active_room_stay_cache_propertyId` + ON `active_room_stay_cache` (`propertyId`) + """.trimIndent() + ) + db.execSQL( + """ + CREATE INDEX IF NOT EXISTS `index_active_room_stay_cache_propertyId_bookingId` + ON `active_room_stay_cache` (`propertyId`, `bookingId`) + """.trimIndent() + ) + } + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/core/LocalDatabaseProvider.kt b/app/src/main/java/com/android/trisolarispms/data/local/core/LocalDatabaseProvider.kt new file mode 100644 index 0000000..2acc77c --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/core/LocalDatabaseProvider.kt @@ -0,0 +1,24 @@ +package com.android.trisolarispms.data.local.core + +import android.content.Context +import androidx.room.Room + +object LocalDatabaseProvider { + @Volatile + private var instance: AppDatabase? = null + + fun get(context: Context): AppDatabase { + return instance ?: synchronized(this) { + instance ?: Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "trisolaris_pms_local.db" + ) + .addMigrations(AppDatabase.MIGRATION_1_2) + .build() + .also { built -> + instance = built + } + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/core/RoomConverters.kt b/app/src/main/java/com/android/trisolarispms/data/local/core/RoomConverters.kt new file mode 100644 index 0000000..935d7e9 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/core/RoomConverters.kt @@ -0,0 +1,29 @@ +package com.android.trisolarispms.data.local.core + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +class RoomConverters { + private val gson = Gson() + private val stringListType = object : TypeToken>() {}.type + private val intListType = object : TypeToken>() {}.type + + @TypeConverter + fun fromStringList(value: List?): String = gson.toJson(value.orEmpty()) + + @TypeConverter + fun toStringList(value: String?): List { + if (value.isNullOrBlank()) return emptyList() + return runCatching { gson.fromJson>(value, stringListType) }.getOrDefault(emptyList()) + } + + @TypeConverter + fun fromIntList(value: List?): String = gson.toJson(value.orEmpty()) + + @TypeConverter + fun toIntList(value: String?): List { + if (value.isNullOrBlank()) return emptyList() + return runCatching { gson.fromJson>(value, intListType) }.getOrDefault(emptyList()) + } +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheDao.kt b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheDao.kt new file mode 100644 index 0000000..d4b0f3e --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheDao.kt @@ -0,0 +1,61 @@ +package com.android.trisolarispms.data.local.roomstay + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow + +@Dao +interface ActiveRoomStayCacheDao { + @Query( + """ + SELECT * FROM active_room_stay_cache + WHERE propertyId = :propertyId + ORDER BY roomNumber ASC, roomStayId ASC + """ + ) + fun observeByProperty(propertyId: String): Flow> + + @Query( + """ + SELECT * FROM active_room_stay_cache + WHERE propertyId = :propertyId AND bookingId = :bookingId + ORDER BY roomNumber ASC, roomStayId ASC + """ + ) + fun observeByBooking(propertyId: String, bookingId: String): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertAll(items: List) + + @Query("DELETE FROM active_room_stay_cache WHERE propertyId = :propertyId") + suspend fun deleteByProperty(propertyId: String) + + @Query("DELETE FROM active_room_stay_cache WHERE propertyId = :propertyId AND roomStayId = :roomStayId") + suspend fun deleteByRoomStay(propertyId: String, roomStayId: String) + + @Query( + """ + UPDATE active_room_stay_cache + SET expectedCheckoutAt = :expectedCheckoutAt, + updatedAtEpochMs = :updatedAtEpochMs + WHERE propertyId = :propertyId AND bookingId = :bookingId + """ + ) + suspend fun updateExpectedCheckoutAtForBooking( + propertyId: String, + bookingId: String, + expectedCheckoutAt: String?, + updatedAtEpochMs: Long + ) + + @Transaction + suspend fun replaceForProperty(propertyId: String, items: List) { + deleteByProperty(propertyId) + if (items.isNotEmpty()) { + upsertAll(items) + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheEntity.kt b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheEntity.kt new file mode 100644 index 0000000..0e02c7c --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayCacheEntity.kt @@ -0,0 +1,71 @@ +package com.android.trisolarispms.data.local.roomstay + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import com.android.trisolarispms.data.api.model.ActiveRoomStayDto + +@Entity( + tableName = "active_room_stay_cache", + indices = [ + Index(value = ["propertyId"]), + Index(value = ["propertyId", "bookingId"]) + ] +) +data class ActiveRoomStayCacheEntity( + @PrimaryKey + val roomStayId: String, + val propertyId: String, + val bookingId: String? = null, + val guestId: String? = null, + val guestName: String? = null, + val guestPhone: String? = null, + val roomId: String? = null, + val roomNumber: Int? = null, + val roomTypeCode: String? = null, + val roomTypeName: String? = null, + val fromAt: String? = null, + val checkinAt: String? = null, + val expectedCheckoutAt: String? = null, + val nightlyRate: Long? = null, + val currency: String? = null, + val updatedAtEpochMs: Long = System.currentTimeMillis() +) + +internal fun ActiveRoomStayDto.toCacheEntity(propertyId: String): ActiveRoomStayCacheEntity? { + val stayId = roomStayId?.trim()?.ifBlank { null } ?: return null + return ActiveRoomStayCacheEntity( + roomStayId = stayId, + propertyId = propertyId, + bookingId = bookingId, + guestId = guestId, + guestName = guestName, + guestPhone = guestPhone, + roomId = roomId, + roomNumber = roomNumber, + roomTypeCode = roomTypeCode, + roomTypeName = roomTypeName, + fromAt = fromAt, + checkinAt = checkinAt, + expectedCheckoutAt = expectedCheckoutAt, + nightlyRate = nightlyRate, + currency = currency + ) +} + +internal fun ActiveRoomStayCacheEntity.toApiModel(): ActiveRoomStayDto = ActiveRoomStayDto( + roomStayId = roomStayId, + bookingId = bookingId, + guestId = guestId, + guestName = guestName, + guestPhone = guestPhone, + roomId = roomId, + roomNumber = roomNumber, + roomTypeCode = roomTypeCode, + roomTypeName = roomTypeName, + fromAt = fromAt, + checkinAt = checkinAt, + expectedCheckoutAt = expectedCheckoutAt, + nightlyRate = nightlyRate, + currency = currency +) diff --git a/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayRepository.kt b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayRepository.kt new file mode 100644 index 0000000..06ce4bd --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/data/local/roomstay/ActiveRoomStayRepository.kt @@ -0,0 +1,48 @@ +package com.android.trisolarispms.data.local.roomstay + +import com.android.trisolarispms.data.api.core.ApiClient +import com.android.trisolarispms.data.api.core.ApiService +import com.android.trisolarispms.data.api.model.ActiveRoomStayDto +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ActiveRoomStayRepository( + private val dao: ActiveRoomStayCacheDao, + private val createApi: () -> ApiService = { ApiClient.create() } +) { + fun observeByProperty(propertyId: String): Flow> = + dao.observeByProperty(propertyId = propertyId).map { rows -> + rows.map { it.toApiModel() } + } + + fun observeByBooking(propertyId: String, bookingId: String): Flow> = + dao.observeByBooking(propertyId = propertyId, bookingId = bookingId).map { rows -> + rows.map { it.toApiModel() } + } + + suspend fun refresh(propertyId: String): Result = runCatching { + val response = createApi().listActiveRoomStays(propertyId = propertyId) + if (!response.isSuccessful) { + throw IllegalStateException("Load failed: ${response.code()}") + } + val rows = response.body().orEmpty().mapNotNull { it.toCacheEntity(propertyId = propertyId) } + dao.replaceForProperty(propertyId = propertyId, items = rows) + } + + suspend fun removeFromCache(propertyId: String, roomStayId: String) { + dao.deleteByRoomStay(propertyId = propertyId, roomStayId = roomStayId) + } + + suspend fun patchExpectedCheckoutForBooking( + propertyId: String, + bookingId: String, + expectedCheckoutAt: String? + ) { + dao.updateExpectedCheckoutAtForBooking( + propertyId = propertyId, + bookingId = bookingId, + expectedCheckoutAt = expectedCheckoutAt, + updatedAtEpochMs = System.currentTimeMillis() + ) + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt index 90a8c66..fcee79a 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt @@ -1,15 +1,28 @@ package com.android.trisolarispms.ui.roomstay -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope import com.android.trisolarispms.data.api.core.ApiClient -import com.android.trisolarispms.core.viewmodel.launchRequest +import com.android.trisolarispms.data.local.core.LocalDatabaseProvider +import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch -class ActiveRoomStaysViewModel : ViewModel() { +class ActiveRoomStaysViewModel( + application: Application +) : AndroidViewModel(application) { private val _state = MutableStateFlow(ActiveRoomStaysState()) val state: StateFlow = _state + private val repository = ActiveRoomStayRepository( + dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao() + ) + private var observeJob: Job? = null + private var observePropertyId: String? = null fun toggleShowOpenBookings() { _state.update { it.copy(showOpenBookings = !it.showOpenBookings) } @@ -21,28 +34,65 @@ class ActiveRoomStaysViewModel : ViewModel() { fun load(propertyId: String) { if (propertyId.isBlank()) return - launchRequest( - state = _state, - setLoading = { it.copy(isLoading = true, error = null) }, - setError = { current, message -> current.copy(isLoading = false, error = message) }, - defaultError = "Load failed" - ) { + observeCache(propertyId = propertyId) + _state.update { it.copy(isLoading = true, error = null) } + viewModelScope.launch { + val activeResult = repository.refresh(propertyId = propertyId) val api = ApiClient.create() - val activeResponse = api.listActiveRoomStays(propertyId) - val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN") - val openBookingsResponse = api.listBookings(propertyId, status = "OPEN") - if (activeResponse.isSuccessful) { - _state.update { - it.copy( - isLoading = false, - items = activeResponse.body().orEmpty(), - checkedInBookings = bookingsResponse.body().orEmpty(), - openBookings = openBookingsResponse.body().orEmpty(), - error = null + val checkedInBookingsResponse = runCatching { + api.listBookings(propertyId = propertyId, status = "CHECKED_IN") + }.getOrNull() + val openBookingsResponse = runCatching { + api.listBookings(propertyId = propertyId, status = "OPEN") + }.getOrNull() + val checkedInBookings = checkedInBookingsResponse + ?.body() + .orEmpty() + .takeIf { checkedInBookingsResponse?.isSuccessful == true } + .orEmpty() + val openBookings = openBookingsResponse + ?.body() + .orEmpty() + .takeIf { openBookingsResponse?.isSuccessful == true } + .orEmpty() + val errorMessage = activeResult.exceptionOrNull()?.localizedMessage + ?: when { + checkedInBookingsResponse != null && !checkedInBookingsResponse.isSuccessful -> + "Load failed: ${checkedInBookingsResponse.code()}" + + openBookingsResponse != null && !openBookingsResponse.isSuccessful -> + "Load failed: ${openBookingsResponse.code()}" + + else -> null + } + _state.update { + it.copy( + isLoading = false, + checkedInBookings = checkedInBookings, + openBookings = openBookings, + error = errorMessage + ) + } + } + } + + override fun onCleared() { + super.onCleared() + observeJob?.cancel() + } + + private fun observeCache(propertyId: String) { + if (observePropertyId == propertyId && observeJob?.isActive == true) return + observeJob?.cancel() + observePropertyId = propertyId + observeJob = viewModelScope.launch { + repository.observeByProperty(propertyId = propertyId).collect { items -> + _state.update { current -> + current.copy( + items = items, + isLoading = if (items.isNotEmpty()) false else current.isLoading ) } - } else { - _state.update { it.copy(isLoading = false, error = "Load failed: ${activeResponse.code()}") } } } } diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt index bcf61da..f126040 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt @@ -63,7 +63,6 @@ import com.android.trisolarispms.data.api.model.BookingBillingMode import com.android.trisolarispms.data.api.model.BookingCancelRequest import com.android.trisolarispms.data.api.model.BookingCheckOutRequest import com.android.trisolarispms.data.api.model.BookingDetailsResponse -import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest import com.android.trisolarispms.ui.booking.BookingDatePickerDialog import com.android.trisolarispms.ui.booking.BookingDateTimeQuickEditorCard import com.android.trisolarispms.ui.booking.BookingTimePickerDialog @@ -211,7 +210,6 @@ fun BookingDetailsTabsScreen( when (page) { 0 -> GuestInfoTabContent( propertyId = propertyId, - bookingId = bookingId, details = detailsState.details, guestId = guestId, isLoading = detailsState.isLoading, @@ -219,7 +217,16 @@ fun BookingDetailsTabsScreen( onEditGuestInfo = onEditGuestInfo, onEditSignature = onEditSignature, onOpenRazorpayQr = onOpenRazorpayQr, - onOpenPayments = onOpenPayments + onOpenPayments = onOpenPayments, + onUpdateExpectedDates = { status, updatedCheckInAt, updatedCheckOutAt -> + detailsViewModel.updateExpectedDates( + propertyId = propertyId, + bookingId = bookingId, + status = status, + updatedCheckInAt = updatedCheckInAt, + updatedCheckOutAt = updatedCheckOutAt + ) + } ) 1 -> BookingRoomStaysTabContent( propertyId = propertyId, @@ -394,7 +401,6 @@ fun BookingDetailsTabsScreen( @Composable private fun GuestInfoTabContent( propertyId: String, - bookingId: String, details: BookingDetailsResponse?, guestId: String?, isLoading: Boolean, @@ -402,7 +408,8 @@ private fun GuestInfoTabContent( onEditGuestInfo: (String) -> Unit, onEditSignature: (String) -> Unit, onOpenRazorpayQr: (Long?, String?) -> Unit, - onOpenPayments: () -> Unit + onOpenPayments: () -> Unit, + onUpdateExpectedDates: suspend (String?, OffsetDateTime?, OffsetDateTime?) -> Result ) { val displayZone = remember { ZoneId.of("Asia/Kolkata") } val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") } @@ -436,37 +443,18 @@ private fun GuestInfoTabContent( } fun submitExpectedDatesUpdate(updatedCheckInAt: OffsetDateTime?, updatedCheckOutAt: OffsetDateTime?) { - val bookingStatus = details?.status?.uppercase() ?: return + if (isUpdatingDates.value) return + val currentStatus = details?.status ?: return + val bookingStatus = currentStatus.uppercase() if (bookingStatus != "OPEN" && bookingStatus != "CHECKED_IN") return scope.launch { isUpdatingDates.value = true updateDatesError.value = null - try { - val body = when (bookingStatus) { - "OPEN" -> BookingExpectedDatesRequest( - expectedCheckInAt = updatedCheckInAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), - expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - ) - else -> BookingExpectedDatesRequest( - expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - ) - } - val response = ApiClient.create().updateExpectedDates( - propertyId = propertyId, - bookingId = bookingId, - body = body - ) - if (response.isSuccessful) { - draftCheckInAt.value = updatedCheckInAt - draftCheckOutAt.value = updatedCheckOutAt - } else { - updateDatesError.value = "Update failed: ${response.code()}" - } - } catch (e: Exception) { - updateDatesError.value = e.localizedMessage ?: "Update failed" - } finally { - isUpdatingDates.value = false + val result = onUpdateExpectedDates(currentStatus, updatedCheckInAt, updatedCheckOutAt) + result.exceptionOrNull()?.let { throwable -> + updateDatesError.value = throwable.localizedMessage ?: "Update failed" } + isUpdatingDates.value = false } } @@ -528,25 +516,21 @@ private fun GuestInfoTabContent( checkOutDateText = zonedCheckOut?.format(dateFormatter) ?: "--/--/----", checkOutTimeText = zonedCheckOut?.format(timeFormatter) ?: "--:--", totalTimeText = formatBookingDurationText(parsedCheckIn, parsedCheckOut), - checkInEditable = canEditCheckIn, - checkOutEditable = canEditCheckOut, + checkInEditable = canEditCheckIn && !isUpdatingDates.value, + checkOutEditable = canEditCheckOut && !isUpdatingDates.value, onCheckInDateClick = { - if (canEditCheckIn) showCheckInDatePicker.value = true + if (canEditCheckIn && !isUpdatingDates.value) showCheckInDatePicker.value = true }, onCheckInTimeClick = { - if (canEditCheckIn) showCheckInTimePicker.value = true + if (canEditCheckIn && !isUpdatingDates.value) showCheckInTimePicker.value = true }, onCheckOutDateClick = { - if (canEditCheckOut) showCheckOutDatePicker.value = true + if (canEditCheckOut && !isUpdatingDates.value) showCheckOutDatePicker.value = true }, onCheckOutTimeClick = { - if (canEditCheckOut) showCheckOutTimePicker.value = true + if (canEditCheckOut && !isUpdatingDates.value) showCheckOutTimePicker.value = true } ) - if (isUpdatingDates.value) { - Spacer(modifier = Modifier.height(8.dp)) - CircularProgressIndicator() - } updateDatesError.value?.let { message -> Spacer(modifier = Modifier.height(8.dp)) Text( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt index dd2fb11..e40953c 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt @@ -1,12 +1,17 @@ package com.android.trisolarispms.ui.roomstay -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiConstants import com.android.trisolarispms.data.api.model.BookingDetailsResponse -import com.android.trisolarispms.core.viewmodel.launchRequest +import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest +import com.android.trisolarispms.data.local.booking.BookingDetailsRepository +import com.android.trisolarispms.data.local.core.LocalDatabaseProvider +import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository import com.google.gson.Gson +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -17,13 +22,25 @@ import okhttp3.Request import okhttp3.sse.EventSource import okhttp3.sse.EventSourceListener import okhttp3.sse.EventSources +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter -class BookingDetailsViewModel : ViewModel() { +class BookingDetailsViewModel( + application: Application +) : AndroidViewModel(application) { private val _state = MutableStateFlow(BookingDetailsState()) val state: StateFlow = _state private val gson = Gson() + private val bookingRepository = BookingDetailsRepository( + dao = LocalDatabaseProvider.get(application).bookingDetailsCacheDao() + ) + private val roomStayRepository = ActiveRoomStayRepository( + dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao() + ) private var eventSource: EventSource? = null private var streamKey: String? = null + private var observeKey: String? = null + private var observeJob: Job? = null private var lastPropertyId: String? = null private var lastBookingId: String? = null private var retryJob: Job? = null @@ -31,25 +48,19 @@ class BookingDetailsViewModel : ViewModel() { fun load(propertyId: String, bookingId: String) { if (propertyId.isBlank() || bookingId.isBlank()) return - launchRequest( - state = _state, - setLoading = { it.copy(isLoading = true, error = null) }, - setError = { current, message -> current.copy(isLoading = false, error = message) }, - defaultError = "Load failed" - ) { - val api = ApiClient.create() - val response = api.getBookingDetails(propertyId, bookingId) - if (response.isSuccessful) { - _state.update { - it.copy( - isLoading = false, - details = response.body(), - error = null - ) + observeCache(propertyId = propertyId, bookingId = bookingId) + _state.update { it.copy(isLoading = true, error = null) } + viewModelScope.launch { + val result = bookingRepository.refreshBookingDetails(propertyId = propertyId, bookingId = bookingId) + result.fold( + onSuccess = { + _state.update { current -> current.copy(isLoading = false, error = null) } + }, + onFailure = { throwable -> + val message = throwable.localizedMessage ?: "Load failed" + _state.update { current -> current.copy(isLoading = false, error = message) } } - } else { - _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } - } + ) } } @@ -80,8 +91,15 @@ class BookingDetailsViewModel : ViewModel() { null } if (details != null) { - _state.update { it.copy(isLoading = false, details = details, error = null) } - retryDelayMs = 2000 + viewModelScope.launch { + bookingRepository.storeSnapshot( + propertyId = propertyId, + bookingId = bookingId, + details = details + ) + _state.update { current -> current.copy(isLoading = false, error = null) } + retryDelayMs = 2000 + } } } @@ -110,11 +128,64 @@ class BookingDetailsViewModel : ViewModel() { retryJob = null } + suspend fun updateExpectedDates( + propertyId: String, + bookingId: String, + status: String?, + updatedCheckInAt: OffsetDateTime?, + updatedCheckOutAt: OffsetDateTime? + ): Result { + val bookingStatus = status?.uppercase() + val body = when (bookingStatus) { + "OPEN" -> BookingExpectedDatesRequest( + expectedCheckInAt = updatedCheckInAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + ) + + "CHECKED_IN" -> BookingExpectedDatesRequest( + expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + ) + + else -> return Result.failure(IllegalStateException("Expected dates are not editable")) + } + val result = bookingRepository.updateExpectedDates( + propertyId = propertyId, + bookingId = bookingId, + body = body + ) + if (result.isSuccess) { + roomStayRepository.patchExpectedCheckoutForBooking( + propertyId = propertyId, + bookingId = bookingId, + expectedCheckoutAt = body.expectedCheckOutAt + ) + } + return result + } + override fun onCleared() { super.onCleared() + observeJob?.cancel() stopStream() } + private fun observeCache(propertyId: String, bookingId: String) { + val key = "$propertyId:$bookingId" + if (observeKey == key && observeJob?.isActive == true) return + observeJob?.cancel() + observeKey = key + observeJob = viewModelScope.launch { + bookingRepository.observeBookingDetails(propertyId = propertyId, bookingId = bookingId).collect { details -> + _state.update { current -> + current.copy( + details = details, + isLoading = if (details != null) false else current.isLoading + ) + } + } + } + } + private fun scheduleReconnect() { val propertyId = lastPropertyId ?: return val bookingId = lastBookingId ?: return diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingRoomStaysViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingRoomStaysViewModel.kt index d8145ab..28bf408 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingRoomStaysViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingRoomStaysViewModel.kt @@ -1,20 +1,29 @@ package com.android.trisolarispms.ui.roomstay -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.model.BookingRoomStayCheckOutRequest -import com.android.trisolarispms.core.viewmodel.launchRequest -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import com.android.trisolarispms.data.local.core.LocalDatabaseProvider +import com.android.trisolarispms.data.local.roomstay.ActiveRoomStayRepository +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class BookingRoomStaysViewModel : ViewModel() { +class BookingRoomStaysViewModel( + application: Application +) : AndroidViewModel(application) { private val _state = MutableStateFlow(BookingRoomStaysState()) val state: StateFlow = _state + private val repository = ActiveRoomStayRepository( + dao = LocalDatabaseProvider.get(application).activeRoomStayCacheDao() + ) + private var observeJob: Job? = null + private var observeKey: String? = null fun toggleShowAll(value: Boolean) { _state.update { it.copy(showAll = value) } @@ -22,38 +31,10 @@ class BookingRoomStaysViewModel : ViewModel() { fun load(propertyId: String, bookingId: String) { if (propertyId.isBlank() || bookingId.isBlank()) return - launchRequest( - state = _state, - setLoading = { it.copy(isLoading = true, error = null) }, - setError = { current, message -> current.copy(isLoading = false, error = message) }, - defaultError = "Load failed" - ) { - val api = ApiClient.create() - val (staysResponse, detailsResponse) = coroutineScope { - val staysDeferred = async { api.listActiveRoomStays(propertyId) } - val detailsDeferred = async { api.getBookingDetails(propertyId, bookingId) } - staysDeferred.await() to detailsDeferred.await() - } - if (!staysResponse.isSuccessful) { - _state.update { it.copy(isLoading = false, error = "Load failed: ${staysResponse.code()}") } - return@launchRequest - } - - val filtered = staysResponse.body().orEmpty().filter { it.bookingId == bookingId } - val details = detailsResponse.body().takeIf { detailsResponse.isSuccessful } - val blockedReason = deriveBookingCheckoutBlockedReason(details) - _state.update { current -> - current.copy( - isLoading = false, - stays = filtered, - error = null, - checkoutBlockedReason = blockedReason, - checkoutError = blockedReason, - conflictRoomStayIds = current.conflictRoomStayIds.intersect( - filtered.mapNotNull { stay -> stay.roomStayId }.toSet() - ) - ) - } + observeBookingCache(propertyId = propertyId, bookingId = bookingId) + _state.update { it.copy(isLoading = true, error = null) } + viewModelScope.launch { + refreshForBooking(propertyId = propertyId, bookingId = bookingId) } } @@ -81,9 +62,9 @@ class BookingRoomStaysViewModel : ViewModel() { ) when { response.isSuccessful -> { + repository.removeFromCache(propertyId = propertyId, roomStayId = roomStayId) _state.update { current -> current.copy( - stays = current.stays.filterNot { it.roomStayId == roomStayId }, checkingOutRoomStayId = null, checkoutError = null, checkoutBlockedReason = null @@ -115,6 +96,57 @@ class BookingRoomStaysViewModel : ViewModel() { } } } + + override fun onCleared() { + super.onCleared() + observeJob?.cancel() + } + + private fun observeBookingCache(propertyId: String, bookingId: String) { + val key = "$propertyId:$bookingId" + if (observeKey == key && observeJob?.isActive == true) return + observeJob?.cancel() + observeKey = key + observeJob = viewModelScope.launch { + repository.observeByBooking(propertyId = propertyId, bookingId = bookingId).collect { stays -> + val stayIds = stays.mapNotNull { it.roomStayId }.toSet() + _state.update { current -> + current.copy( + stays = stays, + isLoading = if (stays.isNotEmpty()) false else current.isLoading, + conflictRoomStayIds = current.conflictRoomStayIds.intersect(stayIds) + ) + } + } + } + } + + private suspend fun refreshForBooking( + propertyId: String, + bookingId: String + ): Result { + val activeResult = repository.refresh(propertyId = propertyId) + val detailsResponse = runCatching { + ApiClient.create().getBookingDetails(propertyId = propertyId, bookingId = bookingId) + }.getOrNull() + val details = detailsResponse?.body().takeIf { detailsResponse?.isSuccessful == true } + val blockedReason = deriveBookingCheckoutBlockedReason(details) + val errorMessage = activeResult.exceptionOrNull()?.localizedMessage + ?: if (detailsResponse != null && !detailsResponse.isSuccessful) { + "Load failed: ${detailsResponse.code()}" + } else { + null + } + _state.update { current -> + current.copy( + isLoading = false, + error = errorMessage, + checkoutBlockedReason = blockedReason, + checkoutError = blockedReason + ) + } + return activeResult + } } private fun handleCheckoutConflict( diff --git a/build.gradle.kts b/build.gradle.kts index 99baeb6..35ab4cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.google.services) apply false -} \ No newline at end of file + alias(libs.plugins.ksp) apply false +} diff --git a/gradle.properties b/gradle.properties index cbb1914..07e0742 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,8 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.builtInKotlin=false +android.newDsl=false systemProp.java.net.preferIPv4Stack=true org.gradle.internal.http.connectionTimeout=600000 org.gradle.internal.http.socketTimeout=600000 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea9408f..e68d368 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,8 @@ lottieCompose = "6.7.1" calendarCompose = "2.6.0" libphonenumber = "8.13.34" zxingCore = "3.5.3" +room = "2.8.4" +ksp = "2.3.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -57,8 +59,13 @@ lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", versio calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendarCompose" } libphonenumber = { group = "com.googlecode.libphonenumber", name = "libphonenumber", version.ref = "libphonenumber" } zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxingCore" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }